Git Product home page Git Product logo

jollygoodcode.github.io's People

Contributors

juanitofatas avatar winston avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jollygoodcode.github.io's Issues

New GitHub Organization API and deppbot

About two weeks ago, we made a change to deppbot.com to reduce the access permissions it has on GitHub organizations and their repos.

Previously, when @deppbot was enabled on an organization repo, deppbot will either (depending on the user's access level):

  • Add deppbot to a "Team with admin access" ("Admin" for short)

OR

  • Create a "Services" team, and add deppbot and the repo to the "Services" team

This actually gave deppbot more access than it required, because both the "Admin" and "Services" teams have read/write access to all the repos in the organization.

It wasn't ideal but that was the only way to do it, before the new GitHub Organization API came along.

With the improved API, it is now possible to add collaborators to an organization repo (previously only possible for user repos) and so, we can do just that without having to add deppbot to any teams!

screen shot 2015-11-26 at 1 23 03 pm

So now if you subscribe a new organization repo on deppbot.com, deppbot will add itself as a collaborator to the repo, instead of relying on teams.

This means deppbot will only have read/write access to the repo and nothing else in the organization.

Related:


Thank you for reading.

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Optimum Sidekiq Configuration on Heroku with Puma

What's the optimum config for Sidekiq on Heroku with Puma?

There are quite a number of answers on the Internet, but nothing definitive, and most of them come with vague numbers and suggestions or are outdated.

Basically, these are the questions that are often asked:

  • What do I exactly put in the config/initializers/sidekiq.rb file?
  • What should I set for client/server size?
  • What should I set for client/server concurrency?
  • How does the Puma workers and threads affect the Sidekiq settings?
  • How does the number of Redis connections affect the Sidekiq settings?
  • How does that number of web/worker dynos affect the Sidekiq settings?

The best (and updated) answers I can find are:

With @bryanrite's post as a reference, this is our Sidekiq config:

config/initializers/sidekiq.rb

require 'sidekiq_calculations'

Sidekiq.configure_client do |config|
  sidekiq_calculations = SidekiqCalculations.new
  sidekiq_calculations.raise_error_for_env!

  config.redis = {
    url: ENV['REDISCLOUD_URL'],
    size: sidekiq_calculations.client_redis_size
  }
end

Sidekiq.configure_server do |config|
  sidekiq_calculations = SidekiqCalculations.new
  sidekiq_calculations.raise_error_for_env!

  config.options[:concurrency] = sidekiq_calculations.server_concurrency_size
  config.redis = {
    url: ENV['REDISCLOUD_URL']
  }
end

lib/sidekiq_calculations.rb

class SidekiqCalculations
  DEFAULT_CLIENT_REDIS_SIZE  = 2
  DEFAULT_SERVER_CONCURRENCY = 25

  def raise_error_for_env!
    return if !Rails.env.production?

    web_dynos
    worker_dynos
    max_redis_connection
  rescue KeyError, TypeError # Integer(nil) raises TypeError
    raise <<-ERROR
Sidekiq Server Configuration failed.
!!!======> Please add ENV:
  - NUMBER_OF_WEB_DYNOS
  - NUMBER_OF_WORKER_DYNOS
  - MAX_REDIS_CONNECTION
    ERROR
  end

  def client_redis_size
    return DEFAULT_CLIENT_REDIS_SIZE if !Rails.env.production?

    puma_workers * (puma_threads/2) * web_dynos
  end

  def server_concurrency_size
    return DEFAULT_SERVER_CONCURRENCY if !Rails.env.production?

    (max_redis_connection - client_redis_size - sidekiq_reserved) / worker_dynos / paranoid_divisor
  end

  private
    def web_dynos
      Integer(ENV.fetch('NUMBER_OF_WEB_DYNOS'))
    end

    def worker_dynos
      Integer(ENV.fetch('NUMBER_OF_WORKER_DYNOS'))
    end

    def max_redis_connection
      Integer(ENV.fetch('MAX_REDIS_CONNECTION'))
    end

    # ENV used in `config/puma.rb` too.
    def puma_workers
      Integer(ENV.fetch("WEB_CONCURRENCY", 2))
    end

    # ENV used in `config/puma.rb` too.
    def puma_threads
      Integer(ENV.fetch("WEB_MAX_THREADS", 5))
    end

    # https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/redis_connection.rb#L12
    def sidekiq_reserved
      5
    end

    # This is added to bring down the value of Concurrency
    # so that there's leeway to grow
    def paranoid_divisor
      2
    end
end

The sidekiq_calculations.rb file is dependent on a number of ENV variables to work, so if you do scale your app (web or workers), do remember to update these ENVs:

  • MAX_REDIS_CONNECTION
  • NUMBER_OF_WEB_DYNOS
  • NUMBER_OF_WORKER_DYNOS

At the same time, WEB_CONCURRENCY and WEB_MAX_THREADS should be the identical ENV variables used to set the number of Puma workers and threads in config/initializers/puma.rb.

Our puma.rb looks exactly like what Heroku has proposed.

The only difference to @bryanrite's calculation is that Sidekiq reserves 5 connections instead of 2 now
according to this line, and I have also added a paranoid_divisor to bring down the concurrency number and keep it below a 80% threshold.

Let me know how this config works for you. Would love to hear your feedback!


Thank you for reading.

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Rails Response and Assets Compression on Heroku

Update 10 Jan 2016: The earlier benchmarks were ran against a Rails 4.2.4 (with Sprockets v3.4.0) app where gzip compression was missing.

@schneems has since reintroduced gzip compression in v3.5.0 (see commit rails/sprockets@7faa6ed), and so I ran the baseline again with Rails 4.2.5 and more importantly with Sprockets v3.5.2. Results:

Baseline Updated with Sprockets v3.5.2

Demo at http://rails-heroku-baseline-updated.herokuapp.com

Let's take another look at our baseline - how a basic Rails 4.2.5 app performs out of the box.

screen shot 2016-01-09 at 11 30 17 pm

As compared to the previous baseline (using Rails 4.2.4 and Sprockets 3.4.0), you can see that in this updated baseline, the application.css and application.js are both gzipped.

screen shot 2016-01-09 at 11 57 28 pm

In total, 571KB was transferred and it took about 3.66s for the page to load.

screen shot 2016-01-10 at 12 00 12 am

When we run Page Speed Insight on this app, we get a score of 64/100 and Enable compression is top of the "Should Fix" list, but it's only for the web request (and not the assets).

This means that we only need to fix the problem of gzipping our web response. Read on!


@heroku is awesome, in that you can deploy a Ruby app in less than 5 minutes up into the internet. However, in exchange for that convenience, we are not able to configure web server settings easily (unless you launch your own Nginx buildpack, for example).

On the other hand, speed really is king and every website aims to be speedier for many, many reasons. Two being for a better user experience and for a better site ranking (according to Google).

One of the most commonly suggested advice in speeding up a website is to enable compression and serve gzipped responses and gzipped assets (JS and CSS) which can be easily configured on the server (Nginx) level.

However, we can't really do that on vanilla Heroku and so we have to explore alternatives.

There are a number of ways we can have content compression on vanilla Heroku, and this post is for exploring those different ways.

tl;dr You can use the heroku-deflater gem.


For the purpose of exploring different ways to achieve content compression on vanilla Heroku, I created a simple Rails 4.2 app with the following gems:

ruby '2.2.3'

gem 'puma'
gem 'pg'

gem 'slim-rails'

gem 'bootstrap-sass'
gem 'font-awesome-sass'

group :staging, :production do
  gem 'rails_12factor'
end

Next, I generated a scaffold for blog_post with title and content as attributes and populated the database 1500 blog posts using seeds.rb.

The source code is available here: https://github.com/winston/rails-heroku-compression

Goals

Our goal is to find out which method is better for achieving compression on:

  • web response
  • application.css
  • application.js

Essentially, these responses should be gzipped and be small in size.

Baseline

Demo at http://rails-heroku-baseline.herokuapp.com

Let's first look at how a basic Rails app performs out of the box.

baseline

The size of the web response is about 431KB, application.css 148KB and application.js 156KB.

In the Content-Encoding column, you can see that all three are not encoded (gzipped) in anyway.

baseline-total

In total, 799KB was transferred and it took about 3.25s for the page to load.

baseline-psi

When we run Page Speed Insight on this app, we get a score of 56/100 and Enable compression is top of the "Should Fix" list.

Rack Deflater

Demo at http://rails-heroku-rack-deflater.herokuapp.com

In this branch, we added a middleware that would perform runtime compression on the web response. However, it doesn't compress CSS or JavaScript.

# Added to config/application.rb

module RailsHerokuCompression
  class Application < Rails::Application
    # ...

    config.middleware.use Rack::Deflater
  end
end

Let's look at how it performs.

rack-deflater

The size of the web response is now 24.5KB and "Content-Encoding" appears as gzip, while application.css and application.js remains unchanged.

That's a saving of about 94% in size!

rack-deflater-total

In total, 392KB was transferred and it took about 3.52s for the page to load.

Even though the total size was reduced by about 50%, however on the average with Rack::Deflater, this branch seemed to have taken just a bit more time than the baseline to load. That's because compression was done during runtime, and that could have resulted in a slight slowdown, as shared by @thoughtbot too.

rack-deflater-psi

When we run Page Speed Insight on this app, we get a score of 70/100 which is an increase of 14 points over baseline.

Assets Gzip

Demo at http://rails-heroku-assets-gzip.herokuapp.com

In this branch, we are only concerned about compressing our assets.

This is important because compression has been removed from Sprockets 3 (affects Rails 4), so we need to do this "manually" for now, until maybe the next version of Sprockets.

Of course, other than doing this on the server, you can explore using a CDN like fastly that could do the compression of assets but we'll leave that to a separate discussion.

# Added to lib/assets.rake
# Source: https://github.com/mattbrictson/rails-template/blob/master/lib/tasks/assets.rake

namespace :assets do
  desc "Create .gz versions of assets"
  task :gzip => :environment do
    zip_types = /\.(?:css|html|js|otf|svg|txt|xml)$/

    public_assets = File.join(
      Rails.root,
      "public",
      Rails.application.config.assets.prefix)

    Dir["#{public_assets}/**/*"].each do |f|
      next unless f =~ zip_types

      mtime = File.mtime(f)
      gz_file = "#{f}.gz"
      next if File.exist?(gz_file) && File.mtime(gz_file) >= mtime

      File.open(gz_file, "wb") do |dest|
        gz = Zlib::GzipWriter.new(dest, Zlib::BEST_COMPRESSION)
        gz.mtime = mtime.to_i
        IO.copy_stream(open(f), gz)
        gz.close
      end

      File.utime(mtime, mtime, gz_file)
    end
  end

  # Hook into existing assets:precompile task
  Rake::Task["assets:precompile"].enhance do
    Rake::Task["assets:gzip"].invoke
  end
end

Let's look at how it performs.

assets-gzip

The web response in this case remains un-gzipped at 431KB.

The size of application.css is now 26.4KB (down from 148KB) and "Content-Encoding" is gzip while the size of application.js is now 48.5KB (down from 156KB) and "Content-Encoding" is gzip too.

assets-gzip-total

In total, 569KB was transferred and it took about 3.22s for the page to load.

assets-gzip-psi

When we run Page Speed Insight on this app, we get a score of 59/100 largely because the web response wasn't compressed.

Heroku Deflater

Demo at http://rails-heroku-heroku-deflater.herokuapp.com

In this branch, we will be using the heroku-deflater gem.

# Added to Gemfile

group: stagimg, :production do
  gem 'heroku-deflater'
end

Let's look at how it performs.

heroku-deflater

The web response is now 24.5KB (down from 431 KB), identical to when Rack::Deflater was used, while application.css is now 26.7KB and application.js is now 49.5KB.

All of them have "Content-Encoding" as gzip.

heroku-deflater-total

In total, 164KB was transferred which translates to a savings of 79% from the baseline measurement, and it took about 2.64s for the page to load.

heroku-deflater-psi

When we run Page Speed Insight on this app, we get a score of 87/100 and it no longer complains about "Compression".

Optimized

Demo at http://rails-heroku-optimized.herokuapp.com

At this point, heroku-deflater has given us the best results so far with everything compressed.

Looking beneath the hood, heroku-deflater is actually simply using Rack::Deflater for "all" requests.
But if a gzipped version of the file already exists, then it would serve up that file immediately and not compressed it every single time.

With this in mind, I decided to try and combine both "Assets Gzip" and "Heroku Deflater" into this branch.

Let's look at how it performs.

optimized

The web response is still compressed at 24.5KB while application.css and application.js are both at the better compression of 26.5KB and 48.5KB (due to "Assets Gzip").

optimized-total

In total, there's also a slight reduction to 163KB sent and it took 2.91s to load the page.

optimized-psi

When we run Page Speed Insight on this app, we get an even more impressive score of 89/100!

Conclusion

App Web Response application.css application.js Total Size Total Time
baseline 431KB 148KB 156KB 799KB 3.25s
rack-deflater 24.5KB 148KB 156KB 392KB 3.52s
assets-gzip 431KB 26.4KB 48.5KB 569KB 3.22s
heroku-deflater 24.5KB 26.7KB 49.5KB 164KB 2.64s
optimized 24.5KB 26.5KB 48.5KB 163KB 2.91s

Rails doesn't do any compression out of the box, and if you are deploying on Heroku, a quick fix would be to use heroku-deflater.

If you are deploying your apps on non-Heroku boxes, then I am sure you will be able to tweak Nginx's server configurations to make compression work even more easily.

Besides doing such compression, it's also a good practice to put your apps behind CDNs too, as that would make your app even speedier.

In summary, don't forget to shrink your app before you deploy!

Notes:


Thank you for reading.

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Ruby on Rails Helpers: h, j, l and t

There are four one-char Rails helpers that seasoned developers use all the time for their simplicity and expressiveness:

h, alias for html_escape

h is a helper for escaping HTML tag characters.

Reference: http://api.rubyonrails.org/classes/ERB/Util.html#method-c-html_escape

> h("is a > 0 & a < 10?")
=> is a &gt; 0 &amp; a &lt; 10?

j, alias for escape_javascript

j is a helper for escaping carriage returns, single and double quotes in JavaScript to prevent malicious JavaScript from being executed via user's input.

Reference: http://api.rubyonrails.org/classes/ActionView/Helpers/JavaScriptHelper.html#method-i-escape_javascript

$('#comment-<%= @comment.id %>').html('<%= j render 'form', comment: @comment %>');

l, alias for localize

l is a helper for localizing Date and Time objects to local formats.

Reference: http://api.rubyonrails.org/classes/AbstractController/Translation.html#method-i-localize

<%= l(Time.current) %>
# => "Wed, 22 Jul 2015 16:35:27 +0800"

t, alias for translate

t is a helper for looking up text translations.

Reference: http://api.rubyonrails.org/classes/AbstractController/Translation.html#method-i-translate

<%= t("site.title") %>
# => "My Awesome Store"

Thanks for reading!

@JuanitoFatas ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Ruby 2.3 in Detail

According to the Ruby Version Policy, Ruby will release a new MINOR version every Christmas as a Christmas gift, and so we are expecting 2.3.0 this year. This release brings many new features and improvements and this post helps you catch up on the changes.

General

Did you mean gem is now bundled by default

Have you ever made a typo, this gem shows the possible corrections for errors like NoMethodError and NameError:

"".downcasr
NoMethodError: undefined method `downcasr' for "":String

    Did you mean? downcase
                  downcase!

Safe operator (&.)

This new operator helps to avoid method invocation on nil.

Consider this:

user && user.name

Can now can be reduced to:

user&.name

Without method name will raise SyntaxError:

obj&. {} # syntax error

Only invoked when needed:

user&.address( telphone() ) # telphone() is conditionally evaluated

Useful for attribute assignment too:

user&.sign_in_count += 1

Benchmark

require "benchmark/ips"

def fast(user = nil)
  user&.email
end

def slow(user = nil)
  user && user.email
end

Benchmark.ips do |x|
  x.report("&.")  { fast }
  x.report("check") { slow }
  x.compare!
end
Calculating -------------------------------------
                  &.   155.950k i/100ms
               check   150.034k i/100ms
-------------------------------------------------
                  &.      7.750M (± 9.1%) i/s -     38.520M
               check      7.556M (± 9.2%) i/s -     37.508M

Comparison:
                  &.:  7750478.7 i/s
               check:  7555733.4 i/s - 1.03x slower

Trivia: Matz also calls this the "lonely operator" in his RubyConf 2015 keynote RubyConf 2015 (a person sitting alone and looking at a dot).

Further Reading:

Frozen String Literal Pragma

Add this Magic Comment to the top of a file, and all Strings in the file will then be frozen by default.

# frozen_string_literal: true

This gem can help to add magic comments to all your ruby files.

Alternatively, you can also:

  • Compile Ruby with --enable=frozen-string-literal or --disable=frozen-string-literal
  • Invoke Ruby with ENV variable like so $ RUBYOPT="--enable=frozen-string-literal" ruby

Known Issue:

Related Issue:

Further Reading:

String

<<~ Indented heredoc

sql = <<~SQL
  SELECT * FROM MICHELIN_RAMENS 
  WHERE STAR = 1
SQL

squish

Active Support's String#squish and String#squish! has been ported to Ruby 2.3.

Module

#deprecate_constant

class Deppbot
  GITHUB_URL = "http://github.com/jollygoodcode/deppbot"

  deprecate_constant :GITHUB_URL

  GITHUB_URI = "https://github.com/jollygoodcode/deppbot"
end

Deppbot::GITHUB_URL will throw a warning: warning: constant Deppbot::GITHUB_URL is deprecated.

Numeric

#positive?

42.positive? # => true

Benchmark

require "benchmark/ips"

class Numeric
  def my_positive?
    self > 0
  end
end

Benchmark.ips do |x|
  x.report("#positive?")  { 1.positive? }
  x.report("#my_positive?") { 1.my_positive? }
  x.compare!
end
Calculating -------------------------------------
          #positive?   166.229k i/100ms
       #my_positive?   167.341k i/100ms
-------------------------------------------------
          #positive?     10.098M (± 9.7%) i/s -     50.035M
       #my_positive?     10.094M (± 8.9%) i/s -     50.035M

Comparison:
          #positive?: 10098105.6 i/s
       #my_positive?: 10093845.8 i/s - 1.00x slower

#negative?

-13.negative? # => true

Benchmark

require "benchmark/ips"

class Numeric
  def my_negative?
    self < 0
  end
end

Benchmark.ips do |x|
  x.report("#negative?")  { 1.negative? }
  x.report("#my_negative?") { 1.my_negative? }
  x.compare!
end
Calculating -------------------------------------
          #negative?   154.580k i/100ms
       #my_negative?   155.471k i/100ms
-------------------------------------------------
          #negative?      9.907M (±10.3%) i/s -     49.002M
       #my_negative?     10.116M (±10.4%) i/s -     50.062M

Comparison:
       #my_negative?: 10116334.8 i/s
          #negative?:  9907112.3 i/s - 1.02x slower

Array

#dig

foo = [[[0, 1]]]

foo.dig(0, 0, 0) => 0

Faster than foo[0] && foo[0][0] && foo[0][0][0]

Benchmark

require "benchmark/ips"

results = [[[1, 2, 3]]]

Benchmark.ips do |x|
  x.report("Array#dig") { results.dig(0, 0, 0) }
  x.report("&&") { results[0] && results[0][0] && results[0][0][0] }
  x.compare!
end
Calculating -------------------------------------
           Array#dig   144.577k i/100ms
                  &&   142.233k i/100ms
-------------------------------------------------
           Array#dig      8.263M (± 8.3%) i/s -     41.060M
                  &&      7.652M (± 9.3%) i/s -     37.976M

Comparison:
           Array#dig:  8262509.7 i/s
                  &&:  7651601.9 i/s - 1.08x slower

#bsearch_index

While Array#bsearch returns a match in sorted array:

[10, 11, 12].bsearch { |x| x < 12 } # => 10

Array#bsearch_index returns the index instead:

[10, 11, 12].bsearch_index { |x| x < 12 } # => 0

Please note that #bsearch and #bsearch_index only works for sorted array.

Struct

#dig

klass = Struct.new(:a)
o = klass.new(klass.new({b: [1, 2, 3]}))

o.dig(:a, :a, :b, 0)              #=> 1
o.dig(:b, 0)                      #=> nil

OpenStruct

OpenStruct is now 3X-10X faster in Ruby 2.3 than it was in earlier versions of Ruby.

If you are using Hashie in any of your libraries (especially API wrappers), now is a good time to switch back to OpenStruct.

Hash

#dig

info = {
  matz: {
    address: {
      street: "MINASWAN street"
    }
  }
}

info.dig(:matz, :address, :street) => 0

Faster than info[:matz] && info[:matz][:address] && info[:matz][:address][:street]

Benchmark

require "benchmark/ips"

info = {
  user: {
    address: {
      street1: "123 Main street"
    }
  }
}

Benchmark.ips do |x|
  x.report("#dig") { info.dig(:user, :address, :street1) }
  x.report("&&") { info[:user] && info[:user][:address] && info[:user][:address][:street1]  }
  x.compare!
end
Calculating -------------------------------------
            Hash#dig   150.463k i/100ms
                  &&   141.490k i/100ms
-------------------------------------------------
            Hash#dig      7.219M (± 8.1%) i/s -     35.961M
                  &&      5.344M (± 7.6%) i/s -     26.600M

Comparison:
            Hash#dig:  7219097.1 i/s
                  &&:  5344038.3 i/s - 1.35x slower

#>, #<, #>=, #<=

Check if a hash's size is larger / smaller than the other hash, or if a hash is a subset (or equals to) of the other hash.

Check this spec on ruby/rubyspec to learn more.

Further Reading:

Note that there isn't Hash#<=>.

#fetch_values

Extract many values from a Hash (fetch_values is similar to values_at):

jollygoodcoders = {
  principal_engineer: "Winston",
  jolly_good_coder: "Juanito",
}

> jollygoodcoders.values_at(:principal_engineer, :jolly_good_coder)
=> ["Winston", "Juanito"]

> jollygoodcoders.fetch_values(:principal_engineer, :jolly_good_coder)
=> ["Winston", "Juanito"]

However, fetch_values will throwKeyError` when a given key is not found in the hash:

> jollygoodcoders.values_at(:principal_engineer, :jolly_good_coder, :project_manager)
=> ["Winston", "Juanito", nil]

> jollygoodcoders.fetch_values(:principal_engineer, :jolly_good_coder, :project_manager)
=> KeyError: key not found: :project_manager

Benchmark

require "benchmark/ips"

jollygoodcoders = {
  principal_engineer: "Winston",
  jolly_good_coder: "Juanito",
}

Benchmark.ips do |x|
  x.report("Hash#values_at") { jollygoodcoders.values_at(:principal_engineer, :jolly_good_coder) }
  x.report("Hash#fetch_values") { jollygoodcoders.fetch_values(:principal_engineer, :jolly_good_coder) }
  x.compare!
end
Calculating -------------------------------------
      Hash#values_at   133.600k i/100ms
   Hash#fetch_values   123.666k i/100ms
-------------------------------------------------
      Hash#values_at      5.869M (± 9.4%) i/s -     29.125M
   Hash#fetch_values      5.583M (± 7.7%) i/s -     27.825M

Comparison:
      Hash#values_at:  5868709.9 i/s
   Hash#fetch_values:  5582932.0 i/s - 1.05x slower

#to_proc

hash = { a: 1, b: 2, c: 3 }

[:a, :b, :c].map &hash
=> [1, 2, 3]

Enumerable

#grep_v

This method returns the inverse of Enumerable#grep. Do not confuse this with the $ grep -v command.

#chunk_while

The positive form of Enumerable#slice_when:

> pi = Math::PI.to_s.scan(/\d/).map(&:to_i)
=> [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3]

> pi.slice_when { |i, j| i.even? != j.even? }.to_a
=> [[3, 1], [4], [1, 5, 9], [2, 6], [5, 3, 5], [8], [9, 7, 9, 3]]

> pi.chunk_while { |i, j| i.even? == j.even? }.to_a
=> [[3, 1], [4], [1, 5, 9], [2, 6], [5, 3, 5], [8], [9, 7, 9, 3]]

Additional Resources

Thanks for reading!

@JuanitoFatas ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Lesser-Known Rails Command-Line Config File

The file ~/.railsrc was introduced in Rails 3.2 in which you are able to specify default command-line options to be used every time when you create a new Rails application with rails new.

This is my .railsrc:

--skip-bundle
--skip-test
--database=postgresql

So when I create a new Rails application, I'll get a Rails application that skips bundle install, skips test files (because I use RSpec most of the time) and use PostgreSQL for database.

You can also pass --no-rc to bypass options in ~/.railsrc, or --rc to specify where the .railsrc file is (if you don't store in the home directory).

Thanks for reading!

@JuanitoFatas ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Update Amazon RDS max_connections with Parameter Group

Do you use Amazon RDS for PostgreSQL?

First, what is max_connections?

Determines the maximum number of concurrent connections to the database server.
max_connections on PostgreSQL Documentation

When running a standby server, you must set this parameter to the same or higher value than on the master server. Otherwise, queries will not be allowed in the standby server.

Do you know that, by default, each instance comes with a different number of max_connections?
Grab a PSQL console to your Postgres database and check now with show max_connections;.

According to this answer on StackExchange (for MySQL), this is how it scales:

MODEL max_connections
t1.micro 34
m1-small 125
m1-large 623
m1-xlarge 1263
m2-xlarge 1441
m2-2xlarge 2900
m2-4xlarge 5816

The numbers in the max_connections column looks slightly awkward because they are actually calculated from a formula DBInstanceClassMemory/magic_number where magic_number differs according to the class of your instance.

To know exactly what's the magic_number for an instance, head over to "Parameter Groups" in your Amazon console:

screen shot 2016-01-28 at 11 05 39 pm

Click on the default Parameter Group and search for max_connections and you'll see the formula.
In my case, it's {DBInstanceClassMemory/31457280}.

screen shot 2016-01-28 at 11 06 10 pm

Fortunately, unlike Heroku Postgres where you can't change any of the Postgres configuration,
you actually can modify Amazon RDS's configuration options!

This means, you can, e.g., increase the max_connections for a t1.micro instance from 34 to 100!

To do that, you can create a new Parameter Group:

screen shot 2016-01-28 at 11 11 21 pm

And update the max_connections to 100:

screen shot 2016-01-28 at 11 11 43 pm

Then, modify your existing instance's DB Parameter Group to use your new Parameter Group:

screen shot 2016-01-28 at 11 13 12 pm

Save and restart your instance, and it should now have 100 connections.

Finally, remember to update your Rails app (database.yml) to make use of these 100 connections.

Resources:

Thanks for reading!

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Encrypt your data with MessageEncryptor

Rails has a built-in class MessageEncryptor, which uses OpenSSL::Cipher to perform encryption. Read How does MessageEncryptor works or the source code if you are interested in its inner workings.

If you have been working with Ruby for a while, you would probably be familiar with attr_accessor, attr_reader and attr_writer which provide you with getter and/or setter. These methods are mostly convenient, but if you are required to store sensitive information in the database (i.e. OAuth token), then you will need your own custom getter and setter to protect the sensitive data.

You can make use of the #encrypt_and_sign and #decrypt_and_verify methods available in Rails to encrypt and decrypt data in your custom getter and setter, and here's how you can do it:

def token=(value)
  encrypted_token = cryptor.encrypt_and_sign(value)
  self[:token] = encrypted_token
end

def token
  encrypted_token = self[:token]

  if encrypted_token.present?
    cryptor.decrypt_and_verify(encrypted_token)
  end
end

private

  def cryptor
    ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base)
  end

Feeling paranoid? You can also pass in additional cipher:

ActiveSupport::MessageEncryptor.new(
  Rails.application.secrets.secret_key_base,
  cipher: "aes-256-ecb"
)

Pro-tip: You can get a list of available ciphers with $ openssl list-cipher-commands.

Testing it is also very simple (with RSpec):

describe "#token=" do
  it "saves encrypted token in database" do
    user = build(:user)
    user.token = "oauth token"

    user.save

    expect(user["token"]).not_to eq("oauth token")
  end
end

describe "#token" do
  it "returns decrypted token upon retrieval" do
    user = build(:user)
    user.token = "oauth token"

    user.save

    expect(user.reload.token).to eq("oauth token")
  end
end

Note that I have used FactoryGirl to perform a build(:user).

Alternatively, if you need a lot more features for your encrypted fields, you can also check out attr_encrypted gem.

Remember to secure sensitive information you store in your database! ❤️

Thanks for reading!

@JuanitoFatas ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Markdown with HTML::Pipeline & Rails

Markdown

Markdown is a lightweight and easy-to-use syntax for styling all forms of writing on the modern web platforms. Checkout this excellent guide by GitHub to learn everything about Markdown.

HTML::Pipeline intro

HTML::Pipeline is HTML Processing filters and utilities. It includes a small framework for defining DOM based content filters and applying them to user provided content. Read an introduction about HTML::Pipeline in this blog post. GitHub uses the HTML::Pipeline to implement markdown.

Implementing Markdown

[ Markdown Content ] -> [ RenderMarkdown ] -> [ HTML ]

Content goes into our pipeline, outputs HTML, as simple as that!

Let's implement RenderMarkdown.

Install HTML::Pipeline & dependency for Markdown

First we'll need to install HTML::Pipeline and associated dependencies for each feature:

# Gemfile
gem "github-markdown"
gem "html-pipeline"

1-min HTML::Pipeline tutorial

require "html/pipeline"
filter = HTML::Pipeline::MarkdownFilter.new("Hi **world**!")
filter.call

Filters can be combined into a pipeline:

pipeline = HTML::Pipeline.new [
  HTML::Pipeline::MarkdownFilter,
  # more filter ...
]
result = pipeline.call "Hi **world**!"
result[:output].to_s

Each filter to hand its output to the next filter's input:

--------------- Pipeline ----------------------
|                                             |
| [Filter 1] -> [Filter 2] ... -> [Filter N]  |
|                                             |
-----------------------------------------------

RenderMarkdown

We can then implement RenderMarkdown class by leveraging HTML::Pipeline:

class RenderMarkdown
  def initialize(content)
    @content = content
  end

  def call
    pipeline = HTML::Pipeline.new [
      HTML::Pipeline::MarkdownFilter
    ]
    pipeline.call(content)[:output].to_s
  end

  private

    attr_reader :content
end

To use it:

RenderMarkdown.new("Hello, **world**!").call
=> "<p>Hello, <strong>world</strong>!</p>"

It works and it is very easy!

Avoid HTML markup

Sometimes users may be tempted to try something like:

<img src='' onerror='alert(1)' />

which is a common trick to create a popup box on the page, we don't want all users to see a popup box.

Due to the nature of Markdown, HTML is allowed. You can use HTML::Pipeline's built-in SanitizationFilter to sanitize.

But the problem with SanitizationFilter is that, disallowed tags are discarded. That is fine for regular use case of "html sanitization" where we want to let users enter some html. But actually We never want HTML. Any HTML entered should be displayed as-is.

For example, writing:

hello <script>i am sam</script>

Should not result in the usual sanitized output (GitHub's behavior):

hello

Instead, it should output (escaped HTML)

hello <script>i am sam</script>

So in here we take a different approach:

We can add a NohtmlFilter, simply replace < to &lt;:

class NoHtmlFilter < TextFilter
  def call
    @text.gsub('<', '&lt;')
    # keep `>` since markdown needs that for blockquotes
  end
end

Put this NoHtmlFilter Before our markdown filter:

class NoHtmlFilter < HTML::Pipeline::TextFilter
  def call
    @text.gsub('<', '&lt;')
  end
end

class RenderMarkdown
  def initialize(content)
    @content = content
  end

  def call
    pipeline = HTML::Pipeline.new [
      NoHtmlFilter,
      HTML::Pipeline::MarkdownFilter,
    ]
    pipeline.call(content)[:output].to_s
  end

  private

    attr_reader :content
end

We keep > since markdown needs that for blockquotes, let's try this:

RenderMarkdown.new("<img src='' onerror='alert(1)' />").call
=> "<p>&lt;img src=&#39;&#39; onerror=&#39;alert(1)&#39; /&gt;</p>"

While <, > got escaped, it still looks the same from user's perspective.

But what if we want to talk about some HTML in code tag?

> content = <<~CONTENT
  > quoted text

  123`<img src='' onerror='alert(1)' />`45678
CONTENT

> RenderMarkdown.new(content).call
=> "<blockquote>\n<p>quoted text</p>\n</blockquote>\n\n<p>123<code>&amp;lt;img src=&#39;&#39; onerror=&#39;alert(1)&#39; /&gt;</code>45678</p>"

The & in the code tag also got escaped, we don't want that. Let's fix this:

class NohtmlMarkdownFilter < HTML::Pipeline::MarkdownFilter
  def call
    while @text.index(unique = SecureRandom.hex); end
    @text.gsub!("<", unique)
    super.gsub(unique, "&lt;")
  end
end

class RenderMarkdown
  def initialize(content)
    @content = content
  end

  def call
    pipeline = HTML::Pipeline.new [
      NohtmlMarkdownFilter,
      HTML::Pipeline::MarkdownFilter,
    ]
    pipeline.call(content)[:output].to_s
  end

  private

    attr_reader :content
end

> RenderMarkdown.new(content).call
=> "<blockquote>\n<p>quoted text</p>\n</blockquote>\n\n<p>123<code>&lt;img src=&#39;&#39; onerror=&#39;alert(1)&#39; /&gt;</code>45678</p>"

This is awesome, but here comes another bug report, autolink does not work anymore:

content = "hey Juanito <[email protected]>"

> RenderMarkdown.new(content).call
=> "<p>hey Juanito <a href=\"mailto:&lt;[email protected]\">&lt;[email protected]</a>&gt;</p>"

The fix is to add a space after our unique string when replacing the <:

class NohtmlMarkdownFilter < HTML::Pipeline::MarkdownFilter
  def call
    while @text.index(unique = "#{SecureRandom.hex} "); end
    @text.gsub!("<", unique)
    super.gsub(unique, "&lt;")
  end
end

class RenderMarkdown
  def initialize(content)
    @content = content
  end

  def call
    pipeline = HTML::Pipeline.new [
      NohtmlMarkdownFilter,
      HTML::Pipeline::MarkdownFilter,
    ]
    pipeline.call(content)[:output].to_s
  end

  private

    attr_reader :content
end

Now autolink works as usual:

content = "hey Juanito <[email protected]>"

> RenderMarkdown.new(content).call
=> "<p>hey Juanito &lt;<a href=\"mailto:[email protected]\">[email protected]</a>&gt;</p>"

But other cases come in. Final version:

class NohtmlMarkdownFilter < HTML::Pipeline::MarkdownFilter
  def call
    while @text.index(unique = SecureRandom.hex); end
    @text.gsub!("<", "#{unique} ")
    super.gsub(Regexp.new("#{unique}\\s?"), "&lt;")
  end
end

Sanitization

While we can display escaped HTML, we still need to add sanitization.

Add SanitizationFilter after our markdown got translated into HTML:

# Gemfile
gem "sanitize"

# RenderMarkdown
class RenderMarkdown
  ...


  def call
    pipeline = HTML::Pipeline.new [
      NohtmlMarkdownFilter,
      HTML::Pipeline::SanitizationFilter,
    ]

    ...
  end

  ...

end

So that our HTML is safe!

Nice to have

Syntax Highlight with Rouge

No more pygements dependency, syntax highlight with Rouge.

# Gemfile
gem "html-pipeline-rouge_filter"

# RenderMarkdown
class RenderMarkdown
  ...


  def call
    pipeline = HTML::Pipeline.new [
      NohtmlMarkdownFilter,
      HTML::Pipeline::SanitizationFilter,
      HTML::Pipeline::RougeFilter
    ]

    ...
  end

  ...

end

Twemoji instead of gemoji (more emojis)

While HTML::Pipeline originally came with an EmojiFilter, which uses gemoji under the hood, there is an alternative solution, twemoji.

# Gemfile
gem "twemoji"

# new file
class EmojiFilter < HTML::Pipeline::Filter
  def call
    Twemoji.parse(doc,
      file_ext: context[:file_ext] || "svg",
      class_name: context[:class_name] || "emoji",
      img_attrs:  context[:img_attrs] || {},
    )
  end
end

# RenderMarkdown
class RenderMarkdown
  ...


  def call
    pipeline = HTML::Pipeline.new [
      NohtmlMarkdownFilter,
      HTML::Pipeline::SanitizationFilter,
      EmojiFilter,
      HTML::Pipeline::RougeFilter
    ]

    ...
  end

  ...

end

Wrap Up

We now have a markdown that can:

  • Can output escaped HTML
  • Syntax highlight with Ruby's Rouge
  • And Better Emoji Support via Twemoji

See JuanitoFatas/markdown@eb7f434...377125 for full implementation!

The Git Data API Awakens

GitHub has a low-level Git Data API. You can do basically everything with Git via this powerful API!

screenshot 2016-08-17 21 47 37

In this tutorial, I am going to walk you through how to use this API with Octokit to change files in one single commit in a new branch and send a Pull Request.

Suppose we want to send a Pull Request for https://github.com/JuanitoFatas/git-playground with these changes:

  • append bar to file foo
  • append baz to file bar
  • in one commit with the message "Update foo & bar in a new topic branch "update-foo-and-bar".

This is how you could do it:

0. Install Octokit Gem

$ gem install octokit

1. Prepare Octokit Client

Get an access token, and open irb with octokit required, then create an Octokit client with your token:

$ irb -r octokit

> client = Octokit::Client.new(access_token: "<your 40 char token>")

We also prepare two variables to be used later, the repo name and new branch name:

repo = "JuanitoFatas/git-playground"
new_branch_name = "update-foo-and-bar"

2. Create a new Topic Branch

First, let's get the base branch (in this case, master branch) SHA1, so that we can branch from master.

We can use the Octokit#refs method to get the base branch SHA1:

master = client.refs(repo).find do |reference|
  "refs/heads/master" == reference.ref
end

base_branch_sha = master.object.sha

And creates a new branch from base branch via Octokit#create_ref method:

new_branch = client.create_ref(repo, "heads/#{new_branch_name}", base_branch_sha)

The tricky part here is that you need to prefix your new branch name with "heads/".

3. Change file contents

First let's use Octokit#contents method with the SHA1 to get existing foo and bar files' content.

foo = client.contents repo, path: "foo", sha: base_branch_sha
bar = client.contents repo, path: "foo", sha: base_branch_sha

Contents on GitHub API is Base64-encoded, we need to decode and append "bar" to foo file, "baz" to bar file respectively:

require "base64"

# path => new content
new_contents = {
  "foo" => Base64.decode64(foo.content) + "bar",
  "bar" => Base64.decode64(bar.content) + "baz"
}

Creates a new tree with our new files (blobs), the new blob can be created via (Octokit#create_blob method). This new tree will be part of our new “tree”.

new_tree = new_contents.map do |path, new_content|
  Hash(
    path: path,
    mode: "100644",
    type: "blob",
    sha: client.create_blob(repo, new_content)
  )
end

4. Create a new commit with changes

Get the current commit first via Octokit#git_commit method:

commit = client.git_commit(repo, new_branch["object"]["sha"])

Note that this method is not the same as Octokit#commit method. git_commit is from the low-level Git Data API, while commit is using the Commits API.

Now we get the commit object, we can retrieve the tree:

tree = commit["tree"]

Creates a new tree by Octokit#create_tree method with the blobs object we created earlier:

new_tree = client.create_tree(repo, new_tree, base_tree: tree["sha"])

The base_tree argument here is important. Pass in this option to update an existing tree with new data.

Now our new tree is ready, we can add a commit onto it:

commit_message = "Update foo & bar"
new_commit = client.create_commit(repo, commit_message, new_tree["sha"], commit["sha"])

5. Add commit to the new branch

Finally, update the reference via Octokit#update_ref method on the new branch:

client.update_ref(repo, "heads/#{new_branch_name}", new_commit["sha"])

6. Issue Pull Request

Creates a new Pull Request via Octokit#create_pull_request method:

title = "Update foo and bar"
body = "This Pull Request appends foo with `bar`, bar with `baz`."
client.create_pull_request(repo, "master", new_branch_name, title, body)

That's it! ✨ See the result here.

Now you can do basically everything with Git via GitHub's Git Data API!

May the Git Data API be with you.

Thanks for reading!

@JuanitoFatas ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Introducing "Automated Security Updates"

🔔 ~ 🔔 ~ 🔔 hor hor hor

We are really excited to announce a new feature for deppbot today 🎉🎊:

Automated Security Updates - Fixes your security vulnerabilities automagically.

See live examples: here, here and here.

The idea behind it is simple if you already know how to Secure Your Ruby App with bundler-audit 🔒.

Let's go through how it works, using discourse/discourse Gemfile@f3e24ba as an example.

First, deppbot uses bundler-audit to find out 🔎 if any gem has security vulnerabilities:

$ git clone [email protected]:discourse/discourse.git && cd discourse
$ bundle-audit
Name: jquery-rails
Version: 3.1.2
Advisory: CVE-2015-1840
Criticality: Medium
URL: https://groups.google.com/forum/#!topic/ruby-security-ann/XIZPbobuwaY
Title: CSRF Vulnerability in jquery-rails
Solution: upgrade to >= 4.0.4, ~> 3.1.3

Name: rest-client
Version: 1.7.2
Advisory: CVE-2015-1820
Criticality: Unknown
URL: https://github.com/rest-client/rest-client/issues/369
Title: rubygem-rest-client: session fixation vulnerability via Set-Cookie headers in 30x redirection responses
Solution: upgrade to >= 1.8.0

Name: rest-client
Version: 1.7.2
Advisory: CVE-2015-3448
Criticality: Unknown
URL: http://www.osvdb.org/show/osvdb/117461
Title: Rest-Client Gem for Ruby logs password information in plaintext
Solution: upgrade to >= 1.7.3

Name: sprockets
Version: 2.11.0
Advisory: CVE-2014-7819
Criticality: Medium
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
Title: Arbitrary file existence disclosure in Sprockets
Solution: upgrade to ~> 2.0.5, ~> 2.1.4, ~> 2.2.3, ~> 2.3.3, ~> 2.4.6, ~> 2.5.1, ~> 2.7.1, ~> 2.8.3, ~> 2.9.4, ~> 2.10.2, ~> 2.11.3, ~> 2.12.3, >= 3.0.0.beta.3

Vulnerabilities found!

We can see that jquery-rails, rest-client, sprockets are vulnerable 🔥🔥🔥 and need to be fixed 💪. As a human, we can choose the appropriate solutions, update Gemfile then bundle again. Well, so does deppbot! 😉.

deppbot will fix this in one commit (just like one would):

Sample discourse automated security updates Commit

But there is more than that! deppbot also provides the information you need to know in the Pull Request:

Sample discourse automated security updates Pull Request

Gems with security vulnerabilities that are fixed are listed at the very top in the Pull Request description, along with the corresponding CVE / OSVDB links to http://rubysec.com.

What about the "With these gem updates" section 😕? You may be wondering why these other gems are updated as well?

Let me explain...

If you take the updated Gemfile, and try to update only the vulnerable gems, you'll see:

$ bundle update jquery-rails sprockets rest-client
Fetching gem metadata from https://rubygems.org/.............
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies......
Bundler could not find compatible versions for gem "sprockets":
  In Gemfile:
    sprockets (~> 2.11.3)

    ember-rails was resolved to 0.18.2, which depends on
      ember-handlebars-template (< 1.0, >= 0.1.1) was resolved to 0.1.5, which depends on
        sprockets (< 3.1, >= 2.1)

    sass-rails (~> 4.0.5) was resolved to 4.0.5, which depends on
      sprockets (<= 2.11.0, ~> 2.8)

    sass-rails (~> 4.0.5) was resolved to 4.0.5, which depends on
      sprockets-rails (~> 2.0.0) was resolved to 2.0.1, which depends on

Oh no, an incompatible error. 😓

However, deppbot is smart enough to figure it out how to resolve it 😎, and gems that are updated to resolve the incompatible error are then placed under the "With these gem updates" section.

When would you receive a Security Update Pull Request? Once deppbot detects vulnerable ruby gems (and there are no open Pull Requests from deppbot), deppbot will issue a Security Update Pull Request regardless of your frequency setting. In this case, we prioritise the security of your app above everything-else and ignore the frequency setting in order to help you secure your app in the quickest time possible.

Let us know what you think about this new feature! 🙇

Merry Christmas 🎄🎁 and Ship Better Software with deppbot in 2016 🎆!

🎅

~ 🔔 ~ 🔔 ~ 🔔

One more thing, 💡 deppbot only works with GitHub repositories with a valid Gemfile and Gemfile.lock.

Twemoji in Rails

If you want to integrate Emoji for your Rails project, Twemoji would be a good choice, gemoji is also available. They're all open sourced and free to use if you properly attribute.

Twemoji provides set of Emoji Keywords (Names) like :heart:, :man::skin-tone-2:, :man-woman-boy::

screenshot 2016-06-09 15 18 13

So you can let your users type these keywords and store the simple string in your database instead of storing the real Unicodes which may be troublesome for some database (read: older version of MySQL).

Integrate with Rails

Install Twemoji:

# Gemfile
gem "twemoji", "~> 3.0.0"

View

And just add a simple View Helper:

module EmojiHelper
  def emojify(content, **options)
    Twemoji.parse(h(content), options).html_safe if content.present?
  end
end

Then in where your content contains emoji, apply this view helper:

<%= emojify post.body %>

In the post.body that all occurrences of emoji keywords will be replaced into Twemoji image.

Twemoji by Twitter provides you scalable SVG images that powered by kind folks from MaxCDN, e.g.:

screenshot 2016-06-09 14 58 40

https://twemoji.maxcdn.com/2/svg/1f60d.svg

PNG is also available of size 72x72: https://twemoji.maxcdn.com/2/72x72/1f60d.png.

Add a little CSS:

img.emoji {
  height: 1em;
  width: 1em;
  margin: 0 .05em 0 .1em;
  vertical-align: -0.1em;
}

and make sure your HTML is unicode-friendly:

<meta charset="utf-8">

Voilà, very simple.

Mailer

In your mailer, you can fallback the SVG images to PNG format by passing in file_ext option:

<%= emojify post.body, file_ext: "png" %>

Front-end

Provide a json which contains all "emoji name to unicode" mappings for your front-end:

# emojis.json.erb
<%=
  Twemoji.codes.map do |code, _|
    Hash(
      value: code,
      html: content_tag(:span, Twemoji.parse(code).html_safe + " #{code}" )
    )
  end.to_json.html_safe
%>

Twemoji gem also provides mappings for SVG and PNG, but they are not loaded by default:

> require "twemoji/svg"
> Twemoji.svg
{
  ":mahjong:"=>"https://twemoji.maxcdn.com/2/svg/1f004.svg",
  ...,
  ":shibuya:" => "https://twemoji.maxcdn.com/2/svg/e50a.svg",
}

> require "twemoji/png"
> Twemoji.png
{
  ":mahjong:"=>"https://twemoji.maxcdn.com/2/72x72/1f004.png",
  ...,
  ":shibuya:" => "https://twemoji.maxcdn.com/2/72x72/e50a.png",
}

If above data fits your use, you can require and use them:

With this json in place, you can then use a autocomplete JavaScript library to implement the autocomlpete feature:

screenshot 2016-06-09 14 58 40

Twemoji also plays nicely if you implement markdown with html-pipeline.

Add a EmojiFilter:

module HTML
  class Pipeline
    module Twitter
      class EmojiFilter < HTML::Pipeline::Filter
        def call
          Twemoji.parse(doc,
            file_ext:   context[:file_ext]   || 'svg',
            class_name: context[:class_name] || 'emoji',
            img_attrs:  context[:img_attrs],
          )
        end
      end
    end
  end
end

and include the EmojiFilter in your filter chain:

HTML::Pipeline.new [
  HTML::Pipeline::MarkdownFilter,
  HTML::Pipeline::SanitizationFilter,
  ...
  HTML::Pipeline::Twitter::EmojiFilter
], { gfm: true, **options }

That's bascially all about integrating Twemoji in Rails.

Test gem with multiple dependencies on Travis CI

Intro

Sometimes you wrote a gem, you need to test it with multiple dependencies, and you use Travis CI. You don't know where to get started, this article is for you!

Gemfile for RubyGem

A RubyGem typically contains a .gemspec and a Gemfile. Personally I specified all runtime dependencies in .gemspec, and all non-runtime dependencies (gems for development, test environments) in Gemfile.

source "https://rubygems.org"

# Specify your gem's dependencies in your-gem.gemspec
gemspec

group :development do
  # development environment dependencies
end

group :test do
  # test environment dependencies
end

Suppose you wrote a gem foo that need to test from Ruby 2.0 to trunk Ruby, and from Rails 3 to Rails 5.

.travis.yml

Test with Multiple Ruby Versions

# .travis.yml

rvm:
  - 2.3.1
  - 2.2.5
  - 2.1
  - 2.0
  - ruby-head

This will run your build with Ruby:

  • Ruby 2.3.1 (2.3.1)
  • Ruby 2.2.5 (2.2.5)
  • Ruby 2.1.x (2.1)
  • Ruby 2.0.x (2.0)
  • Trunk Ruby (ruby-head)

The build will run according to this order. So it is important you put the version you care the most on top (cannot fail), and the version you care the least at bottom (those can fail).

Some builds are impossible to run on Rails 5, like unsuppoted Ruby version (<= 2.0). Some builds are safe to fail, like build with trunk Ruby; You can specify which Ruby versions in .travis.yml that can exclude or can fail:

# .travis.yml

matrix:
  fast_finish: true
  exclude:
    - rvm: 2.0
  allow_failures:
    - rvm: ruby-head

With fast_finish: true in place, as soon as these builds finish:

  • Ruby 2.3.1 (2.3.1)
  • Ruby 2.2.5 (2.2.5)
  • Ruby 2.1.x (2.1)

The whole build will be considered as PASSED / FAILED.

OK. So you now know how to run your build with different Ruby on Travis CI.

How about run the build with different dependencies (gems)?

Test with Multiple Dependencies

Thanks to Bundler, we can specify our dependencies in Gemfile. And Travis CI let you run your build with multiple Gemfile, if we want to test our build from Rails 3 to Rails 5:

gemfile:
  - gemfiles/rails_5.gemfile
  - gemfiles/rails_4.gemfile
  - gemfiles/rails_3.gemfile

Create a folder gemfiles on project root and put rails_3.gemfile, rails_4.gemfile, and rails_5.gemfile files. And you specify your dependencies in each file. Travis will now run your build with Ruby versions you specified combined with these gemfiles:

  • 2.3.1 Rails 3
  • 2.2.5 Rails 3
  • 2.1.x Rails 3
  • 2.0.x Rails 3
  • Trunk Ruby Rails 3
  • 2.3.1 Rails 4
  • 2.2.5 Rails 4
  • 2.1.x Rails 4
  • 2.0.x Rails 4
  • Trunk Ruby Rails 4
  • 2.3.1 Rails 5
  • 2.2.5 Rails 5
  • 2.1.x Rails 5
  • 2.0.x Rails 5
  • Trunk Ruby Rails 5

And you can also specify which Ruby with certain Rails version can fail, for example, Rails 5 required Ruby version 2.2.2, so Ruby version < 2.2.2 will fail:

# .travis.yml

matrix:
  fast_finish: true
  allow_failures:
    - gemfile: gemfiles/rails_5.gemfile
      rvm: 2.1
    - gemfile: gemfiles/rails_5.gemfile
      rvm: 2.0
    - rvm: ruby-head

OK how do we create these gemfiles:

  • Gemfile
  • gemfiles/rails_3.gemfile
  • gemfiles/rails_4.gemfile
  • gemfiles/rails_5.gemfile

We will use thoughtbot's Appraisal tool to do that.

thoughtbot/appraisal

Appraisal runs your tests across configurable, reproducible scenarios that describe variations in dependencies. For example, if you need to test with versions of Rails

First install appraisal gem to development environment:

# Gemfile
source "https://rubygems.org"

gemspec

group :development do
  gem "appraisal"
end

Then creates a Appraisals file with following content:

appraise "rails-3" do
  group :development do
    gem "bundler"
    gem "rake"
  end

  gem "rack", "< 2"
  gem "rails", "3.2.22.2"
end

appraise "rails-4" do
  group :development do
    gem "bundler"
    gem "rake"
  end

  gem "rack", "< 2"
  gem "rails", "~> 4.2.6"
end

appraise "rails-5" do
  group :development do
    gem "bundler"
    gem "rake"
  end

  gem "rails", "~> 5.0.0"
end

And put common dependencies in Gemfile:

source "https://rubygems.org"

gemspec

group :development do
  gem "bundler"
  gem "rake"
  gem "appraisal"
end

then Appraisal file can just be:

appraise "rails-3" do
  gem "rack", "< 2"
  gem "rails", "3.2.22.2"
end

appraise "rails-4" do
  gem "rack", "< 2"
  gem "rails", "~> 4.2.6"
end

appraise "rails-5" do
  gem "rails", "~> 5.0.0"
end

Add:

require "rubygems"
require "bundler/setup"

to the top of your Rakefile.

Run $ appraisal install command, appraisal command-line program will read your Appraisals file and generate multiple Gemfiles to gemfiles folder.

With everything in place, you can now run tests for Rails 3, 4, 5 with appraisal:

$ appraisal rails-3 rake
$ appraisal rails-4 rake
$ appraisal rails-5 rake

Or run tests with all Rails:

$ appraisal rake

To learn more about how appraisal works, please refer to the Appraisal README.md

Wrap Up

You learned...

  • Role of Gemfile and .gemspec for RubyGem
  • How to test with multiple Ruby versions on Travis CI
  • How to test with multiple Gemfiles on Travis CI
  • How to exclude certain build / allow to fail
  • How to use appraisal to generate Gemfiles
  • How to run tests with appraisal

See a real-life example here: gjtorikian/html-pipeline#257.

Automated Updates for RubyGems Repos will end on Feb 26, 2016

Since the beginning, deppbot works for any GitHub repositories that contain a valid Gemfile and lockfile (Gemfile.lock).

While most users subscribed their Ruby/Rails apps on https://www.deppbot.com, we also noticed that some users subscribed their RubyGem repositories. As long as these RubyGem repositories have a valid Gemfile and lockfile, @deppbot will perform its scheduled automated updates on these repositories too.

However, we don't think that this is the optimal practice for such RubyGem repositories.

Yehuda sums up our sentiments excellently in this blog post and we quote:

When developing a gem, use the gemspec method in your Gemfile to avoid duplication. In general, a gem's Gemfile should contain the Rubygems source and a single gemspec line. Do not check your Gemfile.lock into version control, since it enforces precision that does not exist in the gem command, which is used to install gems in practice. Even if the precision could be enforced, you wouldn't want it, since it would prevent people from using your library with versions of its dependencies that are different from the ones you used to develop the gem.

That said, there are RubyGem repositories which also behave like small apps. Typically, these repos have a .gemspec file (which identifies it as a gem), and either a config.ru or Procfile. e.g. attache. It is necessary for such repositories to include both Gemfile and lockfile and be updated continuously.

With that in mind, with effect from 26th Feb, deppbot will stop Automated Updates for all RubyGem repositories because these repositories shouldn't have a Gemfile.lock in the first place, and we believe that deppbot shouldn't perpetuate an unnecessary practice of updating Gemfile.lock in RubyGem repositories. This excludes RubyGem repositories with either config.ru or Procfile present.

Hence, if you have a RubyGem repo subscribed on deppbot, it will eventually be automatically unsubscribed from deppbot.

As a good practice, you might also want to remove the lockfile from your version control and add Gemfile.lock to .gitignore for your RubyGem repos.

If you have any questions, please do not hesitate to let us know (either comment below or email us at [email protected]).

Thank you!
deppbot Team

Building A Simple Mocking Library in Ruby

This tutorial is based on Andy Lindeman's awesome talk — Building a Mocking Library presented at Ancient City Ruby 2013. This is not a direct transcript of the video, but the code presented is almost the same (with minimal changes).

In his talk, Andy showed us how we can build a Mocking library for Minitest with just basic knowledge of Ruby and I felt that it's actually a great way to learn Ruby! So I decided to document the talk in writing and share it up here on the blog so that we can all learn together.

Goal

We are going to implement a simple Mocking library for Minitest.

Given an object:

# Test double
object = Object.new

We should be able to stub a method on this object and it will return our stubbed value:

# Stub
allow(object).to receive(:full?).and_return(true)
object.full? # => true

We should be able to mock an object (mock will verify if removed was ever called, while stub does not do that check):

# Mock
item_id = 1234
assume(object).to receive(:remove).with(item_id)

Why don't we use expect(w).to receive(:remove).with(item_id) here, similar to RSpec? That's because Minitest has an #expect method, so let's avoid redefining it.

Design

We will have two main classes - StubTarget and ExpectationDefinition.

Remember our Goal? In order to be able to do this:

allow(w).to receive(:full?).and_return(true)

We'll break them up as follows using our two main classes:

allow(w).to      receive(:full?).and_return(true)
^^^^^^^^^^^      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#<StubTarget>    #<ExpectationDefinition>

Enough Ruby that You Need to Know

Method Dispatch

How does Ruby find methods? It climbs up the ancestors chain! When you invoke to_s on object object, Ruby asks object...

Ruby: Hey, do you have to_s method?
object: Yup. I do.
Ruby: Awesome! Call it!
object: (invoking to_s)

Then it returns the result of object.to_s which is "#<Object:0x007fc0223b8280>":

> object = Object.new
=> #<Object:0x007fc0223b8280>

> object.to_s
=> "#<Object:0x007fc0223b8280>"

More on Method Dispatch

Ancestors Chain

If Ruby can't find the method you want to call, it will climb up the ancestors chain, till it finds a class that responds to the message, otherwise it eventually throws a NoMethodError exception.

object.class.ancestors
=> [Object, Kernel, BasicObject] # (searching from left to right)

Define a Method for a Specific Object

Ruby has a singleton class for every object and you can define a method in the singleton class.

The singleton class might be not visible in the ancestors chain above, but it's there.

Singleton Class

The "Singleton Class" is easily confused with the Singleton design pattern.

In fact, singleton class is an anonymous class attached to a specific object. Best illustrated with an example:

object = Object.new

def object.hello_world
  "Hello, World!"
end

object.hello_world # => "Hello, World!"

another_object = Object.new

object.hello_world # => NoMethodError (2)

In the example above, we are adding a hello_world method to the object. But the hello_world method wasn't added to the Object class (See (2) above).

As you can see, Ruby insert the hello_world method into object's singleton class!

More on Singleton Class

define_singleton_method

Another way to define a method for singleton class, is to use define_singleton_method(symbol, method_object).

The example above could be re-written as follows:

> object = Object.new
=> #<Object:0x007fc0223b8280>

> object.singleton_class
=> #<Class:#<Object:0x007fc0223b8280>>

> object.define_singleton_method(:hello_world) { "Hello, World!" }

The define_singleton_method accepts a method name and a Method object. Think of a Method object as similar to a proc or lambda.

That's enough Ruby that you need to know. Yup. That's all!

Building a Mock Object Library

Since this is a Mocking library, let's make it a gem!

Your Mocking Gem

Let's gemify our mocking library. You can name it using this pattern: yourname_mock. My name is Juanito and so I will call it juanito_mock, and we'll also use bundle gem command provided by Bundler to create a skeleton of our gem:

$ bundle gem juanito_mock && cd juanito_mock

Note that Bundler may prompt you to choose which test library you want to use, type minitest and hit ENTER.

Creating gem 'juanito_mock'...
MIT License enabled in config
Do you want to generate tests with your gem?
Type 'rspec' or 'minitest' to generate those test files now and in the future. rspec/minitest/(none):

If your generated skeleton gem has no tests or is generated with spec folder, edit ~/.bundle/config file, add this line (or modify):

BUNDLE_GEM__TEST: minitest

Remove the generated folder and repeat it from the top again.

The structure of the gem should look like this:

├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── juanito_mock.gemspec
├── lib
│   ├── juanito_mock
│   │   └── version.rb
│   └── juanito_mock.rb
└── test
    ├── juanito_mock_test.rb
    └── test_helper.rb

Why Minitest?

Since we are implementing a RSpec-like mocking syntax, we don't want to use RSpec here so as to avoid confusions and conflicts with the original RSpec mocking library. Hence, we are going to use Minitest here to test our Mocking library.

By the way, the correct spelling of Minitest is Minitest, not MiniTest.

Renamed MiniTest to Minitest. Your pinkies will thank me.
Minitest 5.0.0 History

Setup Minitest

First lock Minitest to 5.8.0 in gemspec's development dependency in order to use it in development:

spec.add_development_dependency "minitest", "5.8.0"

Latest version of Minitest is 5.8.0 as of 16th Aug 2015.

Add these lines to test/test_helper.rb:

require "minitest/spec"
require "minitest/autorun"

Reorder and your test/test_helper.rb should look like this:

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require "minitest/spec" # simple and clean spec system
require "minitest/autorun" # easy and explicit way to run all your tests
require "juanito_mock"

Note on quotes of string. Just Use double-quoted strings

We use Minitest/Spec syntax to write our tests and require "minitest/autorun" to easily run all our tests.

Next, delete the generated tests in test/juanito_mock_test.rb and update it with DSL:

require "test_helper"

describe JuanitoMock do

end

Now if you run rake, you should have a working test suite:

$ rake
Run options: --seed 55155

# Running:



Finished in 0.000783s, 0.0000 runs/s, 0.0000 assertions/s.

0 runs, 0 assertions, 0 failures, 0 errors, 0 skips

Now let's write our first test!

Implementation of Stub

Create a test case by using it followed by a descriptive description string, and a block of code:

describe JuanitoMock do
  it "allows an object to receive a message and returns a value" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    warehouse.full?.must_equal true
  end
end

Let's walk through the code..

warehouse = Object.new

Firstly, we create a new instance of Object and assign it to a variable warehouse.

allow(warehouse).to receive(:full?).and_return(true)

Then, we create a stub that will receive the method full? and return the result true.

warehouse.full?.must_equal true

Finally, we verify our stub is working by using must_equal.

Sidenote: See the blank lines in our test? These blank lines are very important to distinguish different phases of the test.

More on Four Phase Test

How to Run Tests

First, let's take a look at Rakefile:

require "bundler/gem_tasks"
require "rake/testtask"

Rake::TestTask.new(:test) do |t|
  t.libs << "test"
  t.libs << "lib"
  t.test_files = FileList['test/**/*_test.rb']
end

task :default => :test

require "rake/testtask" included in a file with a rake task defined (rake test) that can run our tests easily via rake, see Rake::TestTask for more information.

You can see a full list of rake tasks available by typing rake -T in your terminal:

$ rake -T
rake build          # Build juanito_mock-0.1.0.gem into the pkg directory
rake install        # Build and install juanito_mock-0.1.0.gem into system ...
rake install:local  # Build and install juanito_mock-0.1.0.gem into system ...
rake release        # Create tag v0.1.0 and build and push juanito_mock-0.1...
rake test           # Run tests

The build, install, install:local, and release tasks are provided by Bundler. See bundler/bundler lib/bundler/gem_helper.rb

But you also see that rake test is available for use.

To make it even simple to run your tests, the Rakefile has this task :default => :test which basically maps the default rake task to running tests.

This means that you can just type rake instead of rake test to run all your tests.

Let's run it:

$ rake
Run options: --seed 49489

# Running:

E

Finished in 0.000963s, 1038.1963 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `allow' for #<#<Class:0x007fe04516bf70>:0x007fe045d001d8>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Yay! Our first failing test, read the error carefully to find out what to do next:

undefined method `allow' for #<#<Class:0x007fe04516bf70>:0x007fe045d001d8>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels) in <top (required)>'

It even tells you which line to fix the code:

test/juanito_mock_test.rb:7

:7 means line 7 from the file test/juanito_mock_test.rb.

Add DSL to Minitest

Let's proceed to fix the failing test.

From Minitest README, we know every test in Minitest is a subclass of Minitest::Test:

To add method allow to Minitest::Test, all we have to do is to create a module TestExtensions and include it in the Minitest::Test class:

require "juanito_mock/version"

module JuanitoMock
  module TestExtensions
    def allow
    end
  end
end

class Minitest::Test
  include JuanitoMock::TestExtensions
end

Let's run our test:

$ rake
Run options: --seed 41300

# Running:

E

Finished in 0.001002s, 997.6067 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
ArgumentError: wrong number of arguments (1 for 0)
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:5:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Nice! Now we get a different error:

ArgumentError: wrong number of arguments (1 for 0)
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:5:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

The error occurs because we are calling allow like so allow(warehouse) in our code, which means we are passing in an argument warehouse which our allow method doesn't accept yet.

allow(warehouse).to receive(:full?).and_return(true)

Let's fix this by modifying our allow method to accept an argument obj. Then, we'll construct an instance of StubTarget with the argument, as described in our design:

require "juanito_mock/version"

module JuanitoMock
  module TestExtensions
    def allow(obj)
      StubTarget.new(obj)
    end
  end
end

class Minitest::Test
  include JuanitoMock::TestExtensions
end

Now run the test again:

$ rake
Run options: --seed 7548

# Running:

E

Finished in 0.000915s, 1092.3434 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NameError: uninitialized constant JuanitoMock::TestExtensions::StubTarget
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:6:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Another error this time:

NameError: uninitialized constant JuanitoMock::TestExtensions::StubTarget
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:6:in `allow'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Ruby is now complaining that it can't find the constant JuanitoMock::TestExtensions::StubTarget. Of course! That's because we haven't define StubTarget class yet, so let's define it:

module JuanitoMock
  class StubTarget
    def initialize(obj)
      @obj = obj
    end
  end

  module TestExtensions
    def allow(obj)
      StubTarget.new(obj)
    end
  end
end

class Minitest::Test
  include JuanitoMock::TestExtensions
end

For a start, we will just save the obj in an instance variable.

Now run the test again:

$ rake
Run options: --seed 25153

# Running:

E

Finished in 0.001059s, 944.4913 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `receive' for #<#<Class:0x007fe080b5d430>:0x007fe081057058>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

What? Another error. This doesn't seem like it's ending soon. But you should actually rejoice, because we now have a different error, and that means we are progressing!

NoMethodError: undefined method `receive' for #<#<Class:0x007fe080b5d430>:0x007fe081057058>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

This time it's ranting about another missing method receive. Hmm what about to? Why didn't it complain about a missing method to?

That's because Ruby always tries to evaluate the right-hand side first, and so it's going to process receive first before it gets to to. Dont' worry, you'll see an error for to later.

Let's define a receive method in TestExtensions module which accepts a message:

require "juanito_mock/version"

module JuanitoMock
  class StubTarget
    ...
  end

  module TestExtensions
    def allow(obj)
      ...
    end

    def receive(message)
      ExpectationDefinition.new(message)
    end
  end
end

As described in design section, receive will return a ExpectationDefinition instance.

Now run the test again:

$ rake
Run options: --seed 27383

# Running:

E

Finished in 0.001145s, 873.0986 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NameError: uninitialized constant JuanitoMock::TestExtensions::ExpectationDefinition
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:16:in `receive'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

You probably already expected it and now Ruby complains that it cannot find ExpectationDefinition:

NameError: uninitialized constant JuanitoMock::TestExtensions::ExpectationDefinition
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:16:in `receive'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Let's go ahead and define it, keeping ExpectationDefinition simple, such that it only accepts an argument and stores it in an instance variable.

module JuanitoMock
  class StubTarget
    ...
  end

  class ExpectationDefinition
    def initialize(message)
      @message = message
    end
  end

  module TestExtensions
    ...
  end
end

Now run the test again:

$ rake
Run options: --seed 47423

# Running:

E

Finished in 0.001005s, 995.4657 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `and_return' for #<JuanitoMock::ExpectationDefinition:0x007fe072f6a798>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Now Ruby cannot find the and_return method
(poor Ruby, thanks for doing so much work for us 😢):

NoMethodError: undefined method `and_return' for #<JuanitoMock::ExpectationDefinition:0x007fe072f6a798>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Looking at the error, it's actually telling us that it cannot find the and_return method on ExpectationDefinition, so let's define it there:

require "juanito_mock/version"

module JuanitoMock
  class StubTarget
    ...
  end

  class ExpectationDefinition
    def initialize(message)
      @message = message
    end

    def and_return(return_value)
      @return_value = return_value
      self
    end
  end

  module TestExtensions
    ...
  end
end

This new method and_return is interesting and contains the secret to enabling method chaining.

Do you know what it is?

Yes. The method is returning self!

def and_return(return_value)
  @return_value = return_value
  self
end

That's the magic to building a chaining interface for your objects! All you have to do is to build up objects and return self!

Now let's run our test again:

$ rake
Run options: --seed 11338

# Running:

E

Finished in 0.001084s, 922.9213 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `to' for #<JuanitoMock::StubTarget:0x007ff48a516590>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Yes! Now we see the error for undefined method to on StubTarget class, and so let's define the to method on StubTarget class according to our design:

allow(warehouse).to receive(:full?).and_return(true)
^^^^^^^^^^^^^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    StubTarget            ExpectationDefinition

where the to method accepts an ExpectationDefinition object as argument.

module JuanitoMock
  class StubTarget
    def initialize(obj)
      @obj = obj
    end

    def to(definition)
    end
  end

  class ExpectationDefinition
    ...
  end

  module TestExtensions
    ...
  end
end

Now let's run our test again:

$ rake
Run options: --seed 31564

# Running:

E

Finished in 0.001092s, 915.9027 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `full?' for #<Object:0x007f8fcc47dc28>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:11:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Reading our next failure, the error guides us to define a full? method on the object:

NoMethodError: undefined method `full?' for #<Object:0x007f8fcc47dc28>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:11:in `block (2 levels) in <top (required)>'

Let's use the aforementioned define_singleton_method magic to define the full? method, and which returns the expected value of true as specified in our test:

module JuanitoMock
  class StubTarget
    ...

    def to(definition)
      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end
  end

  class ExpectationDefinition
    ...
  end

  module TestExtensions
    ...
  end
end

Now run the tests again:

$ rake
Run options: --seed 14082

# Running:

E

Finished in 0.001091s, 916.4954 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `message' for #<JuanitoMock::ExpectationDefinition:0x007ff31d8ae220>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

This time Ruby cannot find message on ExpectationDefinition:

NoMethodError: undefined method `message' for #<JuanitoMock::ExpectationDefinition:0x007ff31d8ae220>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

Keeping it simple, we expose message and return_value in ExpectationDefinition class with attr_reader:

module JuanitoMock
  ...

  class ExpectationDefinition
    attr_reader :message, :return_value

    def initialize(message)
      @message = message
    end

    ...
  end

  ...
end

Now run this test again:

$ rake
Run options: --seed 46498

# Running:

.

Finished in 0.000975s, 1025.2078 runs/s, 1025.2078 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Our first passing test!

WOO-HOO!

allow(warehouse).to receive(:full?).and_return(true)

This line in the test is actually passing! Just with 42 lines of code in a relatively short amount of time!

Let's take a step back to see what we have done so far. Basically, we started to build our Mocking library by writing a test - a failing test. Then we write some code to error that was thrown, and some more code to fix the next error that was thrown and so on and so forth. And finally, through our persistence, we got the test to pass!

This practice of writing software is what we call Test Driven Development (TDD), where we go from red (failing test), to green (passing test) and moving on to refactor. This is a practice that a lot of software engineers embrace, and it's one which we have found immense benefits when doing it consistently.

The Edge of Mocking

edge-of-tomorrow-13

Are we done already? Not quite!

In our current code, we defined the full?method on the object when the test starts, but we didn't do anything to reset our change after the test finishes, and that's actually not so good, because it might affect other tests. So, we should reset the state and unset the full? method that we have "stubbed".

Let's write another test for this:

  it "removes stubbed method after tests finished" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    JuanitoMock.reset

    assert_raises(NoMethodError) { warehouse.full? }
  end
_I intentionally prefix each line with two spaces to make it copy-paste friendly, but I strongly encourage you to type on your own._

In the above test, we invoke JuanitoMock.reset to clear/undo all changes to the code, and we verify this by using assert_raises where we test that an exception is raised
when full? is invoked.

At this point, this is how your test file should look like:

require "test_helper"

describe JuanitoMock do
  it "allows an object to receive a message and returns a value" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    warehouse.full?.must_equal true
  end

  it "removes stubbed method after tests finished" do
    warehouse = Object.new

    allow(warehouse).to receive(:full?).and_return(true)

    JuanitoMock.reset

    assert_raises(NoMethodError) { warehouse.full? }
  end
end

Once again, we run the test to find out what to do next:

$ rake
Run options: --seed 30441

# Running:

.E

Finished in 0.001152s, 1736.7443 runs/s, 868.3721 assertions/s.

  1) Error:
JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `reset' for JuanitoMock:Module
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:17:in `block (2 levels) in <top (required)>'

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Oops. Did you expect that - undefined method reset for JuanitoMock:Module?

Let's write this method! Edit lib/juanito_mock.rb and add this module-level method:

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
  end
end

What do we do next? What should we write in the method? Run the test and let that help us!

$ rake
Run options: --seed 22585

# Running:

.F

Finished in 0.001207s, 1657.1930 runs/s, 1657.1930 assertions/s.

  1) Failure:
JuanitoMock#test_0002_removes stubbed method after tests finished [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:19]:
NoMethodError expected but nothing was raised.

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

NoMethodError expected but nothing was raised.. Yup. That's what I expected, too.

How can we undefine the method full? that we have stubbed on the object? As we had defined the method full? in the object's singleton class, there is actually no reference to it and so we don't know how to undefine it, at least for now...

Let's park that for now, and do something else instead (which would help us later).

Let's improve the code!

We are going to make some changes to our code without losing the any of our current functionality.

Let's start by wrapping the define method step into a class, a delegate class and name it: Stubber. Put Stubber below StubTarget and above the ExpectationDefinition:

module JuanitoMock
  class StubTarget
    ...
  end

  class Stubber
    def initialize(obj)
      @obj = obj
    end

    def stub(definition)
    end
  end

  class ExpectationDefinition
    ...
  end
end

Then, move the implementation of StubTarget#to to Stubber#stub:

module JuanitoMock
  class StubTarget
    ...
  end

  class Stubber
    def initialize(obj)
      @obj = obj
    end

    def stub(definition)
      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end
  end

  class ExpectationDefinition
    ...
  end
end

In StubTarget#to, we delegate the job to Stubber#stub:

module JuanitoMock
  class StubTarget
    ...

    def to(definition)
      Stubber.new(@obj).stub(definition)
    end
  end

  ...
end

Nice refactoring! This is an essential step in the TDD practice, and what we just did was basically to improve our code without modifying the current feature set of our code. We can verify this by running our tests which would prove that the first test is green, while the second test is still red:

$ rake
Run options: --seed 23169

# Running:

.F

Finished in 0.001183s, 1691.2146 runs/s, 1691.2146 assertions/s.

  1) Failure:
JuanitoMock#test_0002_removes stubbed method after tests finished [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:19]:
NoMethodError expected but nothing was raised.

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Let's get back to fixing the error.

Let's store what we stubbed in an array called @definitions, in Stubber#stub:

module JuanitoMock
  ...

  class Stubber
    def initialize(obj)
      @obj = obj
      @definitions = []
    end

    def stub(definition)
      @definitions << definition

      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end
  end

  ...
end

However, having the @definitions array is not enough because the Stubber instance in:

    def to(definition)
      Stubber.new(@obj).stub(definition)
    end

immediately goes out of scope and gets garbage collected, and so we still do not have a list of all methods that were stubbed.

Hence we need to be able to save the Stubber instance(s) by using a Stubber.for_object class-level method:

module JuanitoMock
  class StubTarget
    def initialize(obj)
      @obj = obj
    end

    def to(definition)
      Stubber.for_object(@obj).stub(definition)
    end
  end

  ...
end

Now run the test again:

$ rake
Run options: --seed 37100

# Running:

EE

Finished in 0.001317s, 1518.4864 runs/s, 0.0000 assertions/s.

  1) Error:
JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `for_object' for JuanitoMock::Stubber:Class
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:15:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `for_object' for JuanitoMock::Stubber:Class
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels) in <top (required)>'

2 runs, 0 assertions, 0 failures, 2 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The next error to fix is to define for_object on Stubber class:

  NoMethodError: undefined method `for_object' for JuanitoMock::Stubber:Class
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:10:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:7:in `block (2 levels)

This Stubber.for_object method is a custom initializer for the Stubber class that will not only create Stubber instances, but also store them in a lazily-initialized hash, with its object_id as key:

module JuanitoMock
  class Stubber
    def self.stubbers
      @stubbers ||= {}
    end

    def self.for_object(obj)
      stubbers[obj.__id__] ||= Stubber.new(obj)
    end

    ...
  end
end

But are we making progress for JuanitoMock.reset? Hmm.. Let's run the tests first.

$ rake
Run options: --seed 4701

# Running:

.F

Finished in 0.001117s, 1789.8854 runs/s, 1789.8854 assertions/s.

  1) Failure:
JuanitoMock#test_0002_removes stubbed method after tests finished [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:19]:
NoMethodError expected but nothing was raised.

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The failure is still the same as before, but we are actually making a progress.

Given that all the stubbed methods are now stored in the Stubber class, it would be great if we have one single method in Stubber that can help us. Thinking along that line of thought, let's have a Stubber.reset method that does exactly that, and then it would be trivial for JuanitoMock.reset to invoke it!

Let's try to implement the logic for Stubber.reset that we wish we have.

stubbers currently is a hash that looks like this:

{
  70173643198180 => #<Stubber instance>
}

It is a one-to-one object id mapping to a Stubber instance. We would first want each instance to unstub the method that we stub earlier. The intent is still similar, so each instance should have its own reset method that we can call. Also, the reset method should empty the hash after we are done with it.
Cool! Ruby has a clear method that we can use.

module JuanitoMock
  ...

  class Stubber
    ...

    def self.for_object(obj)
      ...
    end

    def self.reset
      stubbers.each_value(&:reset)
      stubbers.clear
    end

    ...
  end

  ...

  module TestExtensions
    ...
  end

  def self.reset
    Stubber.reset
  end
end

Run the tests again:

$ rake
Run options: --seed 11282

# Running:

.E

Finished in 0.001142s, 1751.6509 runs/s, 875.8255 assertions/s.

  1) Error:
JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `reset' for #<JuanitoMock::Stubber:0x007f9a85c7bf38>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:24:in `each_value'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:24:in `reset'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:66:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:17:in `block (2 levels) in <top (required)>'

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Now we have an undefined method reset for #<JuanitoMock::Stubber:0x007f9a85c7bf38> - a Stubber instance:

JuanitoMock#test_0002_removes stubbed method after tests finished:
NoMethodError: undefined method `reset' for #<JuanitoMock::Stubber:0x007f9a85c7bf38>

Let's implement Stubber#reset. In Stubber#reset, what we need to do is to undefine/unstub the method we have defined/stubbed earlier.

In Ruby, we can use remove_method with some class_eval craziness to achieve this:

module JuanitoMock
  ...

  class Stubber
    ...

    def stub(definition)
      ...
    end

    def reset
      @definitions.each do |definition|
        @obj.singleton_class.class_eval do
          remove_method(definition.message) if method_defined?(definition.message)
        end
      end
    end
  end

  ...
end

We avoid the NoMethodError exception by checking method_defined? on definition.message.

Now if you are brave enough to run the tests:

$ rake
Run options: --seed 55448

# Running:

..

Finished in 0.001233s, 1622.4943 runs/s, 1622.4943 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

You shall see all our tests passed! All green! Yay!!!

gatsby-screenshot2-e1397167545470

All tests passed, old sport! Can we live happily ever after now? Hmm...

Blindly Removing Methods

Till now, we have covered the cases of stubbing and unstubbing. But there's actually a third case to consider!

What if, at the very beginning, there was already a full? method defined? We would have "killed" or "replaced" the original method unknowingly.

Let's write another test to describe this case:

  it "preserves methods that originally existed" do
    warehouse = Object.new
    def warehouse.full?; false; end # defining methods on Ruby singleton class

    allow(warehouse).to receive(:full?).and_return(true)

    JuanitoMock.reset

    warehouse.full?.must_equal false
  end

Run the tests:

$ rake
Run options: --seed 4474

# Running:

.E.

Finished in 0.001266s, 2369.2790 runs/s, 1579.5193 assertions/s.

  1) Error:
JuanitoMock#test_0003_preserves methods that are originally existed:
NoMethodError: undefined method `full?' for #<Object:0x007fcdcd4d3b70>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:29:in `block (2 levels) in <top (required)>'

3 runs, 2 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We have a failure on our new test!

What happened was that our stub allow(warehouse).to receive(:full?).and_return(true) replaced the original method, and when we called JuanitoMock.reset, it only removed the stub but didn't bring back the original implementation for full?.

Hence the NoMethodError exception is expected, because the method's basically gone!

This is not ok. Let's fix that. But first let's take a look at Stubber#stub method:

class Stubber
  ...

  def stub(definition)
    @definitions << definition

    # preserve original method if already exists

    @obj.define_singleton_method definition.message do
      definition.return_value
    end
  end

  ...
end

In our current implementation of Stubber#stub, we didn't check if the object already has the method or not and we just simply (re)defined the singleton method.

We should preserve the original method if it already exists like so:

  class Stubber
    ...

    def stub(definition)
      @definitions << definition

      if @obj.singleton_class.method_defined?(definition.message)
        @preserved_methods <<
          @obj.singleton_class.instance_method(definition.message)
      end

      @obj.define_singleton_method definition.message do
        definition.return_value
      end
    end

    ...
  end

Let's walk through what we just did:

@obj.singleton_class.instance_method(definition.message)

The magic comes from the use of Module#instance_method which will return a method object of given name from the singleton class.

Think of this method object as a proc or lambda which we then we store in a @preserved_methods array:

  class Stubber
    ...

    def initialize(obj)
      @obj = obj
      @definitions = []
      @preserved_methods = []
    end

    ...
  end

Preserving the orignal method is only one part of the solution.

When we do a Stubber#reset, we actually want to reinstate and redefine these saved preserved methods:

  class Stubber
    ...

    def reset
      ...

      @preserved_methods.reverse_each do |method|
        @obj.define_singleton_method(method.name, method)
      end
    end
  end

We use reverse_each here because we need to preserve the original order of the methods. You can write a test here too to see the importance of using reverse_each but we'll leave it as an exercise!

P.S. Did you know reverse_each is more efficient than reverse.each?

In Stubber#stub, we used obj.define_singleton_method with a block, but it also pairs really well with method objects that we are dealing with in the @preserved_methods array.

Every method object Method#instance_method has a Method#name method that returns the name of the method. We can simply redefine the method by calling define_singleton_method with the method name and the method object itself.

Run the tests again and we should have three passing tests:

$ rake
Run options: --seed 63328

# Running:

...

Finished in 0.001223s, 2453.9275 runs/s, 2453.9275 assertions/s.

3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

This also means we have successfully restored the original methods after the tests!

Congrats on getting so far, but we are not quite done. By now we have only implemented stub (and unstub), and next we are going to implement mock - which is an expectation that a message will be received.

To Mock a Mockingbird

mimus_polyglottos1

Let's start with a new failing test as usual:

  it "expects that a message will be received" do
    warehouse = Object.new

    assume(warehouse).to receive(:empty)

    # warehouse.empty not called!

    assert_raises(JuanitoMock::ExpectationNotSatisfied) do
      JuanitoMock.reset
    end
  end

In our test, assume(warehouse).to receive(:empty) expects that warehouse.empty will be invoked. However we are not actually going to call the empty method and so, we assert that a custom exception JuanitoMock::ExpectationNotSatisfied will be raised when we call JuanitoMock.reset which loops and verfies each expectation.

Let's run the test:

$ rake
Run options: --seed 30442

# Running:

..E.

Finished in 0.001308s, 3058.4267 runs/s, 2293.8200 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NoMethodError: undefined method `assume' for #<#<Class:0x007facec071a20>:0x007facecbea5a0>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The first thing we see is:

NoMethodError: undefined method `assume' for #<#<Class:0x007facec071a20>:0x007facecbea5a0>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

We have not define assume in our TestExtensions module, hence the error message. Let's do that! (TestExtensions should be at the bottom of lib/juanito_mock.rb):

module JuanitoMock
  ...

  module TestExtensions
    def allow(obj)
      ...
    end

    def assume(obj)
    end

    def receive(message)
      ...
    end
  end

  def self.reset
    ...
  end
end

Instead of an instance of StubTarget, let's return an instance of ExpectationTarget:

module JuanitoMock
  ...

  module TestExtensions
    ...

    def assume(obj)
      ExpectationTarget.new(obj)
    end

    ...
  end

  def self.reset
    ...
  end
end

Now if you run the tests, it will complain that ExpectationTarget is undefined:

$ rake
Run options: --seed 58985

# Running:

...E

Finished in 0.001235s, 3237.5687 runs/s, 2428.1766 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NameError: uninitialized constant JuanitoMock::TestExtensions::ExpectationTarget
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:79:in `assume'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We use ExpectationTarget in assume because it's a target of a mock expectation (vs. a stub). However, ExpectationTarget is actully very similar to a StubTarget (a specialized form of StubTarget), in that both stubs the original method implementation of an object, but ExpectationTarget does a little something extra by checking that the message has been called.

allow(object).to receive(:message)
assume(object).to receive(:message)

Hence we can make ExpectationTarget a subclass of StubTarget, and let to method in ExpectationTarget inherit the implementation of to method in StubTarget by using super. Then we also store the definition object to a not-yet-exist JuanitoMock.expectations array, so that we can use that to perform our expectation checks later:

module JuanitoMock
  class StubTarget
    ...
  end

  class ExpectationTarget < StubTarget
    def to(definition)
      super
      JuanitoMock.expectations << definition
    end
  end
end

Now run the tests again:

$ rake
Run options: --seed 53163

# Running:

...E

Finished in 0.001286s, 3109.3585 runs/s, 2332.0189 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NoMethodError: undefined method `expectations' for JuanitoMock:Module
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:17:in `to'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:35:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

You know the drill. Let's initialize the expectations array, lazily:

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
    Stubber.reset
  end

  def self.expectations
    @expectations ||= []
  end
end

Run the tests once more and make a little bit more progress:

$ rake
Run options: --seed 51829

# Running:

.E..

Finished in 0.001005s, 3980.0243 runs/s, 2985.0182 assertions/s.

  1) Error:
JuanitoMock#test_0004_expects that a message will be received:
NameError: uninitialized constant JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:39:in `block (2 levels) in <top (required)>'

4 runs, 3 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Where's the class JuanitoMock::ExpectationNotSatisfied? Oops we don't have that yet, so let's fix it:

module JuanitoMock
  ExpectationNotSatisfied = Class.new(StandardError)

  class StubTarget
    ...
  end

  ...
end

Define a simple exception class and run the tests again. You'll see that JuanitoMock::ExpectationNotSatisfied expected but nothing was raised.:

$ rake
Run options: --seed 27046

# Running:

...F

Finished in 0.001435s, 2788.1462 runs/s, 2788.1462 assertions/s.

  1) Failure:
JuanitoMock#test_0004_expects that a message will be received [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:39]:
JuanitoMock::ExpectationNotSatisfied expected but nothing was raised.

4 runs, 4 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

That's expected. We merely created the ExpectationTarget and ExpectationNotSatisfied classes, and we have not added anything new to the Stubber.reset method, so it's right that the new test is failing.

What should Stubber.reset do that would make our test pass? Hmm.. Stubber.reset should be checking that all of our expectations are verified, and would raise an error if any of the expectations failed. Why don't we add a verify method to each Stubber instance that would do the checking?

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
    expectations.each(&:verify)
    Stubber.reset
  end

  def self.expectations
    @expectations ||= []
  end
end

This works, but if an exceptation is raised when verify fails, then Stubber.reset would not actually be executed because the exception would have broke the control flow.

We want to make sure that Stubber.reset is called even if any expectation raised an exception, and we also want to clear @expectations too so that weird things won't happen. Ruby's ensure is here to help:

module JuanitoMock
  ...

  module TestExtensions
    ...
  end

  def self.reset
    expectations.each(&:verify)
  ensure
    expectations.clear
    Stubber.reset
  end

  ...
end

Run the tests again:

$ rake
Run options: --seed 12211

# Running:

...F

Finished in 0.001460s, 2738.9588 runs/s, 2738.9588 assertions/s.

  1) Failure:
JuanitoMock#test_0004_expects that a message will be received [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:39]:
[JuanitoMock::ExpectationNotSatisfied] exception expected, not
Class: <NoMethodError>
Message: <"undefined method `verify' for #<JuanitoMock::ExpectationDefinition:0x007f89bb4b9640>">
---Backtrace---
/Users/Juan/null/juanito_mock/lib/juanito_mock.rb:97:in `each'
/Users/Juan/null/juanito_mock/lib/juanito_mock.rb:97:in `reset'
/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:40:in `block (3 levels) in <top (required)>'
---------------

4 runs, 4 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Getting there! We have an undefined method verify on ExpectationDefinition. Let's do the simplest thing to make the test pass! We'll define the verify method and just raise ExpectationNotSatisfied:

module JuanitoMock
  ...

  class ExpectationDefinition
    ...

    def and_return(return_value)
      @return_value = return_value
      self
    end

    def verify
      raise ExpectationNotSatisfied
    end
  end

  ...
end

Run the tests! All green!

$ rake
Run options: --seed 22996

# Running:

....

Finished in 0.001272s, 3143.7915 runs/s, 3143.7915 assertions/s.

4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

But this is clearly not right even though we have all passing tests. We have a gap in our testing and we'll expose that gap by writing a new test:

  it "does not raise an error if expectations are satisfied" do
    warehouse = Object.new

    assume(warehouse).to receive(:empty)

    warehouse.empty

    JuanitoMock.reset # assert nothing raised!
  end

Now run the tests again:

$ rake
Run options: --seed 46019

# Running:

E....

Finished in 0.001292s, 3870.3166 runs/s, 3096.2532 assertions/s.

  1) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
JuanitoMock::ExpectationNotSatisfied: JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:82:in `verify'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:101:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:101:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:51:in `block (2 levels) in <top (required)>'

5 runs, 4 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

The new test is failing now because verify always raises an exception! That's our cue to implement the actual logic for the verify method which checks if a method has been invoked.

Again, a simple way to solve this would be to use an invocation count as verification, like so:

module JuanitoMock
  ...

  class ExpectationDefinition
    def initialize(message)
      @message = message
      @invocation_count = 0
    end

    ...

    def verify
      if @invocation_count != 1
        raise ExpectationNotSatisfied
      end
    end
  end

  ...
end

But we don't really have a way to increment invocation count. Maybe...

Let's look at the following in Stubber#stub:

@obj.define_singleton_method definition.message do
  definition.return_value
end

When we define the singleton method, we are just simply returning the value via definition.return_value. Instead, let's modify it to look like:

@obj.define_singleton_method definition.message do
  definition.call
end

Invoking a call method is a standard practice if you want an object to act like a callable piece of code, like a proc or lambda (which also has the call method).

Let's run the tests:

$ rake
Run options: --seed 56524

# Running:

.E..E

Finished in 0.001311s, 3815.1804 runs/s, 2289.1082 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `call' for #<JuanitoMock::ExpectationDefinition:0x007fa065d1a030>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:52:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
NoMethodError: undefined method `call' for #<JuanitoMock::ExpectationDefinition:0x007fa065d18b40>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:52:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:49:in `block (2 levels) in <top (required)>'

5 runs, 3 assertions, 0 failures, 2 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Let's add the method call for ExpectationDefinition:

module JuanitoMock
  ...

  class ExpectationDefinition
    ...

    def call
      @invocation_count += 1
      @return_value
    end

    def verify
      ...
    end
  end

  ...
end

The call method will still return the return_value (as was happening earlier with definition.return_value), but at the same time, it also increases the @invocation_count.

Now run the tests again, and we would be all green again!

$ rake
Run options: --seed 47647

# Running:

.....

Finished in 0.001300s, 3846.9144 runs/s, 3077.5315 assertions/s.

5 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Great! We now have basic stub and mock functionality for JuanitoMock. But we don't have yet the ability to pass (and expect) arguments to stubs.

Let's write a test for that:

  it "allows object to receive messages with arguments" do
    warehouse = Object.new

    allow(warehouse).to receive(:include?).with(1234).and_return(true)
    allow(warehouse).to receive(:include?).with(9876).and_return(false)

    warehouse.include?(1234).must_equal true
    warehouse.include?(9876).must_equal false
  end

Now run the tests to see what should we do next:

$ rake
Run options: --seed 17857

# Running:

E.....

Finished in 0.001458s, 4116.0535 runs/s, 2744.0357 assertions/s.

  1) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `with' for #<JuanitoMock::ExpectationDefinition:0x007fb1f2d010f0>
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:57:in `block (2 levels) in <top (required)>'

6 runs, 4 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We don't have the with on ExpectationDefinition. Let's do it:

module JuanitoMock
  ...

  class ExpectationDefinition
    def and_return
      ...
    end

    def with(*arguments)
      @arguments = arguments
      self
    end

    def call
      ...
    end

    ...
  end

  ...
end

The with method will accept an array of arguments, made possible using the splat operator (*), and we also return self to make it chainable.

Run the tests again:

$ rake
Run options: --seed 16039

# Running:

...E..

Finished in 0.001207s, 4969.8536 runs/s, 3313.2358 assertions/s.

  1) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
ArgumentError: wrong number of arguments (1 for 0)
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:51:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'

6 runs, 4 assertions, 0 failures, 1 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Now we see ArgumentError: wrong number of arguments (1 for 0).

Let's decrypt this error message.

What it's saying is that you passed in 1 argument, but the method defined only requires 0 arguments.

Luckily there is also a line number telling us where things went wrong:

lib/juanito_mock.rb:51:in `block in stub'

Line 51 or Stubber#stub should be:

@obj.define_singleton_method definition.message do
  definition.call
end

Let's allow the define_singleton_method block to accept splat arguments as well:

@obj.define_singleton_method definition.message do |*arguments|
  definition.call
end

Run the tests again:

$ rake
Run options: --seed 3190

# Running:

..F...

Finished in 0.001697s, 3536.4931 runs/s, 2947.0775 assertions/s.

  1) Failure:
JuanitoMock#test_0006_allows object to receive messages with arguments [/Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60]:
Expected: true
  Actual: false

6 runs, 5 assertions, 1 failures, 0 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

And we now have a failure on our test:

  warehouse.include?(1234).must_equal true
  warehouse.include?(9876).must_equal false
end

Let's look at the test again:

it "allows object to receive messages with arguments" do
  warehouse = Object.new

  allow(warehouse).to receive(:include?).with(1234).and_return(true)
  allow(warehouse).to receive(:include?).with(9876).and_return(false)

  warehouse.include?(1234).must_equal true
  warehouse.include?(9876).must_equal false
end

warehouse.include?(1234) is returning false (and failing the test). That's because we have yet to do any matching on the stub argument and so the last stub

allow(warehouse).to receive(:include?).with(9876).and_return(false)

is the one that's being returned, no matter what arguments are used.

Why is the last stub returned? Remember when we defined the singleton method:

@obj.define_singleton_method definition.message do |*arguments|
  definition.call
end

We only invoked a definition via definition.call, but we didn't actually invoke the right definition.

Similar to our reset method, we should (reverse) search and find the definition that matches the method name and arguments:

@obj.define_singleton_method definition.message do |*arguments|
  @definitions
    .reverse
    .find { |definition| definition.matches(definition.message, *arguments) }
    .call
end

Run the tests again:

$ rake
Run options: --seed 61311

# Running:

E.EEE.

Finished in 0.001315s, 4563.6538 runs/s, 1521.2179 assertions/s.

  1) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:49:in `block (2 levels) in <top (required)>'


  3) Error:
JuanitoMock#test_0003_preserves methods that are originally existed:
JuanitoMock::ExpectationNotSatisfied: JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:97:in `verify'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:117:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:117:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:27:in `block (2 levels) in <top (required)>'


  4) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'

6 runs, 2 assertions, 0 failures, 4 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

Yikes. 4 tests failed! Let's take a look at the last one:

JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `reverse' for nil:NilClass
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:54:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'

Hmm. Let's look at our implementation again:

@obj.define_singleton_method definition.message do |*arguments|
  @definitions
    .reverse
    .find { |definition| definition.matches(definition.message, *arguments) }
    .call
end

Why is @definitions nil? That's because self has changed, in a singleton method block like this:

@obj.define_singleton_method definition.message do |*arguments|
  ...
end

And because instance variables (@definitions) are looked up on self (which is now @obj and not the outer instance), @definitions is something different (and unintialized) in the block. We call this a closure.

An easy fix would be to create a temporary variable:

module JuanitoMock
  ...

  class Stubber
    ...

    def stub(definition)
      ...

      definitions = @definitions
      @obj.define_singleton_method definition.message do |*arguments|
        definitions
          .reverse
          .find { |definition| definition.matches(definition.message, *arguments) }
          .call
      end
    end

    def reset
      ...
    end
  end

  ...
end

Run the tests again:

$ rake
Run options: --seed 52635

# Running:

..EEEE

Finished in 0.001867s, 3214.3833 runs/s, 1071.4611 assertions/s.

  1) Error:
JuanitoMock#test_0006_allows object to receive messages with arguments:
NoMethodError: undefined method `matches' for #<JuanitoMock::ExpectationDefinition:0x007ff542c9bf00>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block (2 levels) in stub'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `find'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:60:in `block (2 levels) in <top (required)>'


  2) Error:
JuanitoMock#test_0005_does not raise an error if expectations are satisfied:
NoMethodError: undefined method `matches' for #<JuanitoMock::ExpectationDefinition:0x007ff542c9b7d0>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block (2 levels) in stub'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `find'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:49:in `block (2 levels) in <top (required)>'


  3) Error:
JuanitoMock#test_0003_preserves methods that are originally existed:
JuanitoMock::ExpectationNotSatisfied: JuanitoMock::ExpectationNotSatisfied
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:98:in `verify'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:118:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:118:in `reset'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:27:in `block (2 levels) in <top (required)>'


  4) Error:
JuanitoMock#test_0001_allows an object to receive a message and returns a value:
NoMethodError: undefined method `matches' for #<JuanitoMock::ExpectationDefinition:0x007ff542c9ad30>
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block (2 levels) in stub'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `each'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `find'
    /Users/Juan/null/juanito_mock/lib/juanito_mock.rb:55:in `block in stub'
    /Users/Juan/null/juanito_mock/test/juanito_mock_test.rb:9:in `block (2 levels) in <top (required)>'

6 runs, 2 assertions, 0 failures, 4 errors, 0 skips

rake aborted!
Command failed with status (1): [ruby -I"lib:test:lib"  "/Users/Juan/.rubies/ruby-2.2.2/lib/ruby/2.2.0/rake/rake_test_loader.rb" "test/juanito_mock_test.rb" ]

Tasks: TOP => default => test
(See full trace by running task with --trace)

We got rid of the nil error, and now we have an undefined method matches in ExpectationDefinition!

This is the final step, I promise:

  class ExpectationDefinition
    ...

    def with(*arguments)
      ...
    end

    def matches(message, *arguments)
      message == @message &&
        (@arguments.nil?) || arguments == @arguments
    end

    def call
      ...
    end

    ...
  end

Again, we'll run all the tests:

$ rake
Run options: --seed 14495

# Running:

......

Finished in 0.001514s, 3963.4020 runs/s, 3963.4020 assertions/s.

6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Now we have ALL OUR TESTS PASSING!

C O N G R A T U L A T I O N S

You've got a basic mocking library!

This library is pretty good now, except with some caveats...

  • with(...) and calling it with different arguments raises NoMethodError
  • define_singleton_method and singleton_class are on Object and so stubbing on BasicObject is not supported
  • private methods are not preserved
  • reset method should be invoked automatically at the end of each test (teardown)

Luckily, RSpec already addressed these and more, so you can just use rspec-mocks.

Further Reading:

Thank you for reading!

Happy Mocking!

Till next time 😘
Juanito Fatas,
Edits by Winston Teo Yong Wei

If you want to tweet or share this tutorial, don't forget to mention and thank @alindeman!
All credits go to him! I only did the writing here. 😉

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

The Birth of deppbot

I started using Ruby and Rails in 2007, versions 1.8 and 2.x respectively at that time. Being new to engineering as a career and also to the language and framework, I didn't know much about engineering best practices at that time, and I just focused on making sure I got things done in whatever ways that worked.

That worked out pretty ok. The app worked. The company progressed. As far as I can remember, I didn't update/upgrade the code base much. The only times when I did bundle update rails were probably due to security fixes. I shunned away from doing any form of updates because some things would break unknowingly as I don't have automated regression tests to warn me of side effects.

That continued for about another 2+ years before I joined Pivotal Labs. In Pivotal Labs, I learned a lot more about engineering best practices and one of which is Testing. It's important to have automated tests because of many reasons, i.e. prevent regressions etc. And if you can go another level up, doing Test Driven Development is even better.

This post is not about Testing though, but it's related to another best practice that I learned. One of keeping your code base updated always, including all the gems that you are using!

Always Update Your Code

Why is that important?

It's common knowledge now that software projects never ends, and so, your team will probably be working on the same code base for many years. Case in point, Shopify has a 11 year old code base, quote:

Shopify now runs on Rails 4.2. Same codebase started on Rails 0.5 in August 2004 roughly 11 years ago.

Because of that, there are several reasons it's important to keep your code base updated always:

Improvements

There are improvements made to Ruby and Rails everyday through the hardwork of the dedicated community, and every version update allows the language or framework to better cope with the increasing demands of new apps nowadays. At the same time, most RubyGems have frequent updates too - both feature improvements and bugs fixes.

Hence doing frequent updates allow applications to take advantage of these improvements which could possibly make applications perform better (in terms of speed and memory) or make engineers more productive (nothing legacy to maintain).

Hiring

Would you like to work on an old code base? Some people might really like the challenge. I would prefer not to, and most of the people I know would want to work on a well-maintained updated code base.

Moreover..

You would also need to hire folks with intimate experience of the language and framework, to be able to support and maintain the code base and that limits your search scope for talents.

And it would be difficult to onboard junior developers because they would probably have been learning the new and shiny stuff and have very little clue on how to work with older versions of the language or framework.

More time will be spent hiring or training and that's not time well-spent.

So, it's better to keep your code base updated for as it aids hiring and training.

Easier Now Than Later

You'll need to update your code sooner or later, for various reasons like a security fix in a latest version of a dependency, or for a new feature that's easier done with a new RubyGem. So, why wait?

Based on my experience since the Pivotal Lab days, it's pretty painful to only do the updates when you need it in a big bang approach and the typical scenario goes like this..

Maybe you set out to only upgrade one single dependency, but as it turns out, it also requires a new version of another dependency and so on and so forth - a chain reaction ensues.

Your app could be broken by any of these changes and it takes time to hunt down the offender. You end up spending a lot more time on the update (making code base changes, fixing bugs) while the rest of team continues the development.

Oops.. Now you'll get conflicting changes between the update branch and the continued development on master. Next, you have to deal with the pain of merging your updates back into master.

What was supposedly simple turns out to be not so simple afterall. Hence it's really easier to update your app iteratively, in smaller steps than to do it in a big bang approach.

A Good Engineering Discipline

Last but not least, imo, it's just simply a good engineering discipline. Why would you let your code base decay?

There may be times when you should lock your dependencies to specific versions, but there should always be a strong reason to back that up. Otherwise, my belief is always to keep it updated to the lastest version.


Frequent Updates as a Practice

Can everyone put this into practice though? The answer is "No".

Remember my story about my first engineering job? I wouldn't have been able to do this in my first job, simply because my app doesn't have tests. Hence, if I do frequent updates (e.g. once per week), I would have to spend more time running manual tests after each update to make sure everything still worked as expected, and that would just be unscalable. And so fundamentally, I need to fix the problems of (lack of) testing in that case.

But if you are already have a great test coverage, I strongly encourage you to do frequent updates!

The Manual Way

In Pivotal Labs, we used Jenkins as the CI server and so we would have two builds for each project.

The first build would run on every commit to master:

# set up repo
bundle install
rake spec

The second build would run every midnight on master:

# set up repo
bundle update
rake spec

Then in the morning we would be able to see if master's all good with updated gems. If it is, we will do a manual bundle update locally and push it up.

Works, but a pretty manual still. And as humans, sometimes we procrastinate or forget, and so we might only be updating the app once per 2 weeks?

The Service Way

Since moving to other cloud CIs, I missed the functionality of scheduling builds at specific times. But more importantly, I really hope to do this more consistently and spread this practice more (especially to my clients).

Hence we built deppbot - https://www.deppbot.com!

deppbot will help us run bundle update daily and issue a Pull Request (PR) for it. As we have GitHub hooked to our CI (Codeship), tests will automatically run when a PR is issued, and so we can simply merge the PR into master if everything is green. Simple. Convenient.

The Pull Request also comes with a great description of the updated changes (using GitHub's compare view) so it's easy to see the changes of the gems and be sure that nothing is out of the extraordinary.

We have been running this for a while with internal projects and clients, and we really love it, so we feel it's time to release it to the public.

Give it a try now at https://www.deppbot.com - Free for all public repos, and paid plans are inexpensive too.

Although it's built as a SaaS, deppbot is really more about the practice to keep your code updated (and which is easier when you have specs), than just a service that performs bundle update. It's a practice that we believe in and we hope all teams do too, as we know it will make the team happier. :)


Epilogue

Maybe you might be thinking though:

I don't dare to do automated daily updates even if I have tests. I would prefer to do it manually so that I can test it before merging

These are the questions that I have though:

  • How frequent will you update if you do it manually?
  • What would make you more confident of the updates if you do it manually?
  • What would you test differently if you do it manually?
  • Why don't you encode that difference as an automated test then?

Think again. Are manual updates really better?

Why don't you give deppbot a try? 😘

deppbot

Thanks for reading!

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Secure Your Ruby / Rails App with bundler-audit

bundler-audit

bundler-audit is a gem which provides patch-level verification for Bundler.

When you use Bundler, a lockfile Gemfile.lock will be generated in your project,
and bundler-audit scans your Gemfile.lock to see if you are:

  • Using a vulnerable version of a gem
  • Installing gems from an insecure source such as http:// or git@

Let's see how we can use bundler-audit.

First, install bundler-audit:

$ gem install bundler-audit

Let's take a look at an example. The following is the output ran against jollygoodcode/dasherize's Gemfile@1eaf973:

$ bundle-audit
Insecure Source URI found: git://github.com/rails/turbolinks.git
Vulnerabilities found!

Note that the command is bundle-audit instead of bundler-audit.

bundler-audit is warning us that an "Insecure Source URI" has been found, and that's because a gem is installed from an insecure source git://github.com which could be subjected to MITM attacks.

The solution is to either install the gem from https:// or use a released gem.

How does bundler-audit knows about all the vulnerabilities?

Beneath the hood, bundler-audit is using data from ruby-advisory-db to check your Gemfile.lock. And while bundler-audit comes with a vendored data, you should update the ruby-advisory-db data everytime before you run bundle-audit:

$ bundle-audit update

Hook bundler-audit to your CI Workflow

It's easy to integrate bundler-audit as part of your CI workflow,
and the following steps work for any Ruby projects (doesn't have to be Rails).

First, add a rake Task:

$ touch lib/bundler/audit/task.rb

With following content:

require "rake/tasklib"

module Bundler
  module Audit
    class Task < Rake::TaskLib
      def initialize
        define
      end

      protected

      def define
        namespace :bundle do
          desc "Updates the ruby-advisory-db then runs bundle-audit"
          task :audit do
            require "bundler/audit/cli"
            %w(update check).each do |command|
              Bundler::Audit::CLI.start [command]
            end
          end
        end
      end
    end
  end
end

If you run your specs or tests with rake, add this to Rakefile:

require_relative "lib/bundler/audit/task"
Bundler::Audit::Task.new

task default: "bundle:audit"

Or any other form of rake file: rakefile, Rakefile, rakefile.rb, Rakefile.rb.

Now when you run rake with this new rake task, rake will first run your tests,
and then update ruby-advisory-db before executing bundle-audit.

Secure your app with bundler-audit today!

The bundler-audit is brought to you by rubysec, kudos to @rubysec & @postmodern.

Thanks for reading!

@JuanitoFatas ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Dasherize, Turbolinks 3 and Parallel

We open-sourced Dasherize a few days ago.

screenshot 2015-10-08 18 48 13

Dasherize is a simple, material-designed dashboard for your projects on which you can see:

  • CI status of master branch (supports Travis CI, Codeship and CircleCI)
  • GitHub Pull Requests and Issues count and a peek of most recents

More importantly, Dasherize also has a presentation mode for big screen displays.

The README has more details of how Dasherize came about, so you can read that.

This blog post dives more into the technical details.

Turbolinks 3

Dasherize 3 uses Turbolinks 3 🎩. In fact, it's tracking master of Turbolinks now.

Specifically, it uses the Partial Replacement ✨ technique that's only available in Turbolinks 3.

Which Feature?

Turbolinks is used to load each "Card" on the dashboard.

1

Code Walk Through

When the dashboard loads, it first fills the dashboard with empty "Cards" (name only) for each project.

The code can be found in app/views/projects/index.html.slim, and the loop is:

- if @projects.present?
  .row.mar-lg-top
    - @projects.each do |project|
      .col.s12.m4
        .project id="project:#{project.id}"
          = link_to project_path(project), project_path(project), remote: true, class: 'hide js-project'

          .card-panel.no-padding.grey.darken-1
            .card-heading
              .card-title
                = link_to project.repo_name
                .right
                  = link_to icon("gear"), edit_project_path(project), class: "gear"
            .card-status.center.progress
              .indeterminate

The important bit are the two lines below, while the rest are just markup that creates an empty "Card" with a progress bar.

.project id="project:#{project.id}"
  = link_to project_path(project), project_path(project), remote: true, class: 'hide js-project'

The id is important because this is the id to be used for Turbolinks Partial Replacement, so that a specific .project can be swapped out with a server response.

Next, the anchor tag links to the project_path(project) which is a RESTful path to projects#show that shows (the "Card" for) one project.

The magic happens with remote: true and some JavaScript. When the page loads, JavaScript will trigger a click on all the anchor tags with .js-project class.

// app/assets/javascripts/projects.js

$(".js-project").not('.in-progress').addClass('in-progress').click();

As each link has remote: true, each click results in an async call to projects#show which looks like:

# app/controllers/projects_controller.rb

def show
  @project = ProjectDecorator.new(@project)
  @project.process_with(current_user.oauth_account.oauth_token)

  render change: "project:#{@project.id}"
end

If you noticed, the last line of the method show reads render change: "project:#{@project.id}".

Let's break it down:

render with change instructs Turbolinks 3 to render the response (instead of doing a normal page load).

change: "project:#{@project.id}" instructs Turbolinks 3 to replace only the div with a matching id that can be found in the rendering app/views/projects/show.html.slim.

And so, one by one, the empty "Cards" will be replaced by "Cards" with information.

As of this writing, Turbolinks 3's Partial Replacement technique looks really promising to me. In fact, before Turbolinks 3, I would write custom JS that sort of mimics the behavior of Partial Replacement. Hence I am really looking forward to the release of Turbolinks 5 as that means I don't need to write extra JS anymore.

There is a potential problem which I am keeping track of though:

turbolinks/turbolinks-classic#546

Parallel Tests

You are probably familiar with Parallel Tests but not so much of the gem that powers it: Parallel.

If you look into the source code, you will notice that I am actually not storing anything in the database (except for projects). Hence in order to make API calls ((GitHub + CI) * Number of Projects) speedy, Parallel is used to parallelize the API calls.

Back to app/controllers/projects_controller.rb again, where we first instantiate a ProjectDecorator, then we invoke process_with with the user's GitHub OAuth token:

# app/controllers/projects_controller.rb

def show
  @project = ProjectDecorator.new(@project)
  @project.process_with(current_user.oauth_account.oauth_token)

  render change: "project:#{@project.id}"
end

The implementation of process_with is as follows:

# app/models/project_decorator

def process_with(oauth_token=nil)
  @oauth_token = oauth_token

  call_apis
end

The magic in this case happens in the private method call_apis which invokes other methods:

def call_apis
  Parallel.each(api_functions, in_threads: api_functions.size) { |func| func.call }
end

def api_functions
  [
    method(:init_repos),
    method(:init_ci)
  ]
end

def init_repos
  client   = Octokit::Client.new(access_token: @oauth_token)
  @_issues = client.issues(repo_name)
end

def init_ci
  @_ci =
    case ci_type
      when "travis"
        Status::Travis.new(repo_name, travis_token).run!
      when "codeship"
        Status::Codeship.new(repo_name, codeship_uuid).run!
      when "circleci"
        Status::Circleci.new(repo_name, circleci_token).run!
      else
        Status::Null.new
    end
end    

In the method call_apis, Parallel was used to fork 2 threads (api_functions.size), and to split and execute the methods in api_functions in separate threads.

def call_apis
  Parallel.each(api_functions, in_threads: api_functions.size) { |func| func.call }
end

Using method(:init_repos) and method(:init_ci), these two methods become function pointers that we can pass it as arguments to Parallel.each and be eventually invoked with func.call.

As such, to call both GitHub and CI apis for a project, no waiting is required to make the two api calls. With Parallel, it helped to reduce the time required for making all API calls, and thus made the dashboard load speedily.

I had fun building Dasherize as a toy utility project.

I hope you enjoyed reading about some of the technical details too. 😊 Thanks for reading!

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Free SSL with CloudFlare and Heroku

Recently, I set up CloudFlare with Heroku to make good use of its Universal SSL and essentially made Dasherize https the poor man's way.

My aim was to get the following working:

  • http://dasherize.com redirects to https://www.dasherize.com
  • http://www.dasherize.com redirects to https://www.dasherize.com
  • https://dasherize.com redirects to https://www.dasherize.com
  • https://www.dasherize.com works!

Here are the steps to get that working:

1) Sign up for a CloudFlare account

Go to https://www.cloudflare.com/.

2) Add a website to CloudFlare

1

3) Configure CNAME

After you have scanned your website, you will probably see an A entry and a CNAME entry.

Modify (and/or delete) the A and CNAME entries so that they become:

  • CNAME, dasherize.com to Heroku domain name
  • CNAME, www to Heroku domain name

It might look strange to have two CNAME going to the same Heroku domain, but CloudFlare supports CNAME Flattening so we are good.

2

4) Wait for DNS to propagate

At this point, we can wait for DNS to propagate and when it's done:

  • http://dasherize.com redirects to https://dasherize.com
  • http://www.dasherize.com redirects to https://www.dasherize.com

And the DNS entries should look like so (with some information ommitted):

$ curl -I http://dasherize.com
HTTP/1.1 301 Moved Permanently
...
Location: https://dasherize.com/
Via: 1.1 vegur
Server: cloudflare-nginx

$ curl -I http://www.dasherize.com
HTTP/1.1 301 Moved Permanently
...
Location: https://www.dasherize.com/
Via: 1.1 vegur
Server: cloudflare-nginx

$ curl -I https://dasherize.com
HTTP/1.1 200 OK
Server: cloudflare-nginx
...
Via: 1.1 vegur

$ curl -I https://www.dasherize.com
HTTP/1.1 200 OK
Server: cloudflare-nginx
..
Via: 1.1 vegur

We are almost there, we are just left with redirecting http://dasherize.com to https://www.dasherize.com.

You might be thinking.. But why www? Everyone has different opinions.

5) Final Redirection

To redirect http://dasherize.com to https://www.dasherize.com, we need to include add a Page Rule that:

3

Forwards (301) https://dasherize.com/* to https://www.dasherize.com

And with that, the DNS entries will look like:

$ curl -I http://dasherize.com
HTTP/1.1 301 Moved Permanently
...
Location: https://dasherize.com/
Via: 1.1 vegur
Server: cloudflare-nginx

$ curl -I http://www.dasherize.com
HTTP/1.1 301 Moved Permanently
...
Location: https://www.dasherize.com/
Via: 1.1 vegur
Server: cloudflare-nginx

$ curl -I https://dasherize.com
HTTP/1.1 301 Moved Permanently
...
Server: cloudflare-nginx
Location: https://www.dasherize.com/

$ curl -I https://www.dasherize.com
HTTP/1.1 200 OK
...
Server: cloudflare-nginx
Via: 1.1 vegur

6) Full SSL

Finally, go to the Crypto page, and make sure that you have selected the Full option for your SSL. You can read more about the differences by clicking on Help below the select options.

screen shot 2015-11-16 at 10 35 20 pm


With these 6 steps, you now have a SSL enabled site for $0, all thanks to CloudFlare's Full SSL option:

4

Since all Heroku apps comes free with https and that, quoting CloudFlare, "CloudFlare will not attempt to validate the certificate", hence it makes it easy for us to have the Dasherize site SSL-enabled.

Also, don't forget to set config.force_ssl = true in your Rails production.rb.


Thank you for reading.

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Read-Only OAuth Scope on GitHub, Please?

We love @github. Our processes all revolve around GitHub.

Naturally by extension, we love the GitHub API, because it allows us to do creative things with GitHub.

So far, we have built a few apps that rely heavily on GitHub's API:

  • deppbot
    • A service that does automated dependency updates for Ruby apps, issued as Pull Requests
  • Dasherize
    • A simple dashboard for CI and GitHub stats
  • GitHub Gem Stats
    • A toy app for "Which RubyGems does X use?" and "Which GitHub repo uses Y gem?"

Let's talk about permissions next.

For both deppbot and Dasherize, we require access to both public and private repos.

Looking at GitHub's OAuth scopes, we'll need to use the repo scope.

screen shot 2015-10-16 at 12 21 19 pm

Hmm.. But wait a minute.. The repo scope grants read AND write access to basically everything! Getting read access is probably a must for all apps, but do we need write on everything?

Due to the nature of deppbot, we'll need write permission on public and private repos, so that it can issue Pull Requests when it finishes the dependency update for a project and perform other actions.

However, all Dasherize does is read from public or private repos, and it's not doing any write at all. You can even take a look at the source code to verify that.

So isn't it intrusive to require write permission too? Definitely.

As a user, I would like all apps to only require the lowest level of permission that it needs to operate.

As a developer, I am taking on unnecessary liability when my app has permissions that it doesn't need.

Of course, we are not the first to create apps that use GitHub API, and this has been a common issue for both users and app developers for a while, for example:

By design, GitHub API does not provide any Read-only OAuth scope for public and/or private repos. Once you ask for permissions to either public and/or private repos, you'll get both read and write. What can we do then if we just want Read-only access on GitHub API?

There are definitely work arounds, as mentioned in some of the links above:

Progressive Permissioning

This means that the app shall only ask for permissions when it requires it.

Let's use @houndci as an example.

When you first sign up, @houndci only asks for access to your email and public repos read/write.

screen shot 2015-10-16 at 4 18 16 pm

Then, it provides you with the option to "Include Private Repos".

screen shot 2015-10-16 at 4 19 44 pm

Clicking on that, you can now grant @houndci access to both public and private repos read/write.

screen shot 2015-10-16 at 4 20 19 pm

In this way, you only grant @houndci necessary permissions when it requires it.

But this still doesn't solve the problem if my app just requires a read scope, like Dasherize..

Manual Setup

Alternatively, maybe a manual setup of collaborators might help?

screen shot 2015-10-18 at 3 03 18 pm

Unfortunately not.

When you add a collaborator to a GitHub repo, the collaborator naturally has read and write permissions, and you can't change it.

What about Teams (for Organization repos only)? Can it grant Read-only permissions?

Yes. That might help!

You can create a special Team in the organization, grant the Team a read-only access to the repo,
and now you have a Read-only scope. But in most cases, manual setup is not the best UX experience. 😢

screen shot 2015-10-16 at 4 30 39 pm

Recently, GitHub also added Read-only Deploy Keys, as another option to grant Read-only access to one single repo.

Many are speculating that this eventually lead to a Read-only OAuth scope. I sure hope so.


In summary, we really hope that @github can provide developers with a Read-only OAuth scope, so that app developers don't have to explain ourselves every time we use the repo scope.

In both deppbot and Dasherize, we are conscious of our decision in asking for read write access to public and private repos because we went with the simplest solution for now to validate the ideas. Definitely, we should look into both Progressive Permissing or Manual Setup when the apps get enough traction and feedback from users.

Thank you for reading.

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Find Unused Code

We will use a CLI tool to find unused code automatically 🔎🔎🔎.

Unused

Unused is a CLI tool to find unused code, written in 100% Haskell by awesome Josh Clayton.

Install Unused on macOS

$ brew tap joshuaclayton/formulae
$ brew install unused

Ctags for macOS

Install Ctags via homebrew instead of using system built-in older Ctags:

$ brew install ctags

If you having problem that your Ctags does not load homebrew-installed one, add an alias (zsh):

alias ctags="`brew --prefix`/bin/ctags"

Generate index file

For Ruby files here is an example:

$ ctags -f .git/tags -R $(git ls-files | grep .rb)

A .tags file under .git folder will be generated, which contains the index of your Ruby code. Don't forget to gitignore your tags file.

Find Unused Code

$ unused -s -g none
* -s,--single-occurrence   Display only single occurrences
* -g,--group-by ARG        [Allowed: directory, term, file, none] Group results

unused will do the hard work, find where unused code are, and report to you:

BulkCustomerDashboard                     1, 1  app/dashboards/bulk_customer_dashboard.rb  used once
UserSerializer                            1, 1  app/serializers/user_serializer.rb         used once
authorized_repos                          1, 1  lib/github_api.rb                          used once
create_subscription_record                1, 1  app/services/repo_subscriber.rb            used once
excluded_file?                            1, 1  lib/ext/scss-lint/config.rb                used once
has_something?                            1, 1  spec/models/linter/ruby_spec.rb            used once
register_email                            1, 1  spec/models/linter/ruby_spec.rb            used once
stub_commit                               1, 1  spec/services/build_runner_spec.rb         used once
stub_customer_with_discount_find_request  1, 1  spec/support/helpers/stripe_api_helper.rb  used once
stubbed_style_checker_with_config_file    1, 1  spec/services/build_runner_spec.rb         used once
token=                                    1, 1  app/models/user.rb                         used once
user_names                                1, 1  spec/models/linter/ruby_spec.rb            used once
verified_request?                         1, 1  app/controllers/application_controller.rb  used once

Wow, unused code found, delete them and then sent a Pull Request!

Happy Housekeeping! 🏡

Note that Unused is meant to guide, though, not give definitive answers to what can be removed.
-- Josh Clayton ‏@joshuaclayton

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.