jollygoodcode / jollygoodcode.github.io Goto Github PK
View Code? Open in Web Editor NEW:thought_balloon: Jolly Good Blog
Home Page: https://jollygoodcode.github.io
:thought_balloon: Jolly Good Blog
Home Page: https://jollygoodcode.github.io
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):
OR
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!
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.
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.
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:
config/initializers/sidekiq.rb
file?size
?concurrency
?The best (and updated) answers I can find are:
With @bryanrite's post as a reference, this is our Sidekiq config:
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
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.
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 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:
Let's take another look at our baseline - how a basic Rails 4.2.5 app performs out of the box.
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.
In total, 571KB was transferred and it took about 3.66s for the page to load.
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
Our goal is to find out which method is better for achieving compression on:
application.css
application.js
Essentially, these responses should be gzipped and be small in size.
Let's first look at how a basic Rails app performs out of the box.
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.
In total, 799KB was transferred and it took about 3.25s for the page to load.
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.
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.
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!
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.
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.
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.
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.
In total, 569KB was transferred and it took about 3.22s for the page to load.
When we run Page Speed Insight on this app, we get a score of 59/100 largely because the web response wasn't compressed.
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.
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
.
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.
When we run Page Speed Insight on this app, we get a score of 87/100 and it no longer complains about "Compression".
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.
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").
In total, there's also a slight reduction to 163KB sent and it took 2.91s to load the page.
When we run Page Speed Insight on this app, we get an even more impressive score of 89/100!
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.
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.
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 > 0 & a < 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.
$('#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
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.
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.
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!
&.
)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:
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:
--enable=frozen-string-literal
or --disable=frozen-string-literal
$ RUBYOPT="--enable=frozen-string-literal" ruby
Known Issue:
Related Issue:
Further Reading:
String
<<~
Indented heredocsql = <<~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 throw
KeyError` 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]]
Thanks for reading!
@JuanitoFatas ✏️ 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 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
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.
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:
Click on the default Parameter Group and search for max_connections
and you'll see the formula.
In my case, it's {DBInstanceClassMemory/31457280}
.
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:
And update the max_connections
to 100:
Then, modify your existing instance's DB Parameter Group to use your new Parameter Group:
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!
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 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
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 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 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.
[ Markdown Content ] -> [ RenderMarkdown ] -> [ HTML ]
Content goes into our pipeline, outputs HTML, as simple as that!
Let's implement RenderMarkdown
.
HTML::Pipeline
& dependency for MarkdownFirst we'll need to install HTML::Pipeline
and associated dependencies for each feature:
# Gemfile
gem "github-markdown"
gem "html-pipeline"
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!
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 <
:
class NoHtmlFilter < TextFilter
def call
@text.gsub('<', '<')
# 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('<', '<')
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><img src='' onerror='alert(1)' /></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>&lt;img src='' onerror='alert(1)' /></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, "<")
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><img src='' onerror='alert(1)' /></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:<[email protected]\"><[email protected]</a>></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, "<")
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 <<a href=\"mailto:[email protected]\">[email protected]</a>></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?"), "<")
end
end
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!
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
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
We now have a markdown that can:
See JuanitoFatas/markdown@eb7f434...377125 for full implementation!
GitHub has a low-level Git Data API. You can do basically everything with Git
via this powerful API!
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:
bar
to file foobaz
to file barThis is how you could do it:
$ gem install octokit
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"
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/"
.
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
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"])
Finally, update the reference via Octokit#update_ref
method on the new branch:
client.update_ref(repo, "heads/#{new_branch_name}", new_commit["sha"])
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
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.
🔔 ~ 🔔 ~ 🔔 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):
But there is more than that! deppbot also provides the information you need to know in the 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.
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:
:
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).
Install Twemoji:
# Gemfile
gem "twemoji", "~> 3.0.0"
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.:
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.
In your mailer, you can fallback the SVG images to PNG format by passing in file_ext
option:
<%= emojify post.body, file_ext: "png" %>
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:
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.
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!
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
rvm:
- 2.3.1
- 2.2.5
- 2.1
- 2.0
- ruby-head
This will run your build with Ruby:
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:
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)?
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:
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.
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 Gemfile
s 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
You learned...
Gemfile
and .gemspec
for RubyGemGemfile
s on Travis CIappraisal
to generate Gemfile
sSee a real-life example here: gjtorikian/html-pipeline#257.
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
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.
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.
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>
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>"
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)
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.
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!
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!
Since this is a Mocking library, let's make it a 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
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
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!
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.
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
.
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!
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.
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
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!!!
All tests passed, old sport! Can we live happily ever after now? Hmm...
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.
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 supportedprivate
methods are not preservedreset
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:
allow/expect
discussion in RSpec: rspec/rspec-mocks#153Thank 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. 😉
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.
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!
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:
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).
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.
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.
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.
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!
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?
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. :)
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:
Think again. Are manual updates really better?
Why don't you give deppbot a try? 😘
Thanks for reading!
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.
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:
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
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
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.
We open-sourced Dasherize a few days ago.
Dasherize is a simple, material-designed dashboard for your projects on which you can see:
master
branch (supports Travis CI, Codeship and CircleCI)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.
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.
Turbolinks is used to load each "Card" on the dashboard.
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
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!
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.
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:
Go to https://www.cloudflare.com/.
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 nameCNAME
, www
to Heroku domain nameIt might look strange to have two CNAME
going to the same Heroku domain, but CloudFlare supports CNAME Flattening so we are good.
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.
To redirect http://dasherize.com
to https://www.dasherize.com
, we need to include add a Page Rule
that:
Forwards (301)
https://dasherize.com/*
tohttps://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
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.
With these 6 steps, you now have a SSL enabled site for $0, all thanks to CloudFlare's Full SSL option:
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.
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.
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:
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.
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:
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
.
Then, it provides you with the option to "Include Private Repos".
Clicking on that, you can now grant @houndci access to both public and private repos read
/write
.
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..
Alternatively, maybe a manual setup of collaborators might help?
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. 😢
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.
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.
We will use a CLI tool to find unused code automatically 🔎🔎🔎.
Unused is a CLI tool to find unused code, written in 100% Haskell by awesome Josh Clayton.
$ brew tap joshuaclayton/formulae
$ brew install unused
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"
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.
$ 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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.