RSpec: Increase readability with super_diff
When handling nested hashes the RSpec output is often hard to read. Here the gem super_diff
could help.
Add super_diff to your project
- Add
super_diff
to your Gemfile:
gem 'super_diff'
- Require it in your
spec_helper.rb
require 'super_diff/rspec' # For Rails applications you can replace this with 'super_diff/rspec-rails'
-
Customize colors in
spec/support/super_diff.rb
SuperDiff.configure do |config|
config.ac...
Learn how to use ruby/debug
This talk shows simple and advanced usages of the ruby/debug debugger. It goes through a step by step debugging workflow.
Here are some command examples:
(rdbg) step 2 # step twice
(rdbg) info # show current scope, including self
(rdbg) bt # show backtrace
(rdbg) frame 3 # go directly to frame 3
(rdbg) break User#email # add a breakpoint in the email instance method
(rdbg) catch SomeException # break when SomeException is raised
Some advanced exam...
Using the ActiveSupport::BroadcastLogger
The ActiveSupport::BroadcastLogger allows you to log to multiple sinks. You know this behavior from from the rails server
command, that both logs to standard out and the log/development.log
file.
Here is an example from the ActiveSupport::BroadcastLogger
API:
stdout_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("development.log")
broadcast = ActiveSupport::BroadcastLogger.new(stdout_logger, file_logger)
broadcast.i...
How to allow testing beforeunload confirmation dialogs with modern ChromeDrivers
Starting with ChromeDriver 127, if your application displays a beforeunload
confirmation dialog, ChromeDriver will immediately close it. In consequence, any automated tests which try to interact with unload prompts will fail.
This is because ChromeDriver now follows the W3C WebDriver spec which states that any unload prompts should be closed automatically.
However, this applies only to "HTTP" test sessions, i.e. what you're using by default. The spec also defines that bi-directional test se...
High-level data types with "composed_of"
I recently stumbled upon the Rails feature composed_of. One of our applications dealt with a lot of addresses and they were implemented as 7 separate columns in the DB and Rails models. This seemed like a perfect use case to try out this feature.
TLDR
The feature is still a VERY leaky abstraction. I ran into a lot of ugly edge cases.
It also doesn't solve the question of UI. We like to use
simple_form
. It's currently not possible to simply write `f...
A simple example with a GIN index in Rails for optimizing a ILIKE query
You can improve your LIKE
/ ILIKE
search queries in PostgreSQL by adding a GIN index with an operate class ("opclass") to split the words into trigrams to the required columns.
Example
class AddSearchTextIndexToUsers < ActiveRecord::Migration[7.1]
def change
enable_extension 'pg_trgm'
add_index :users, :search_tex...
Logging multiple lines in Rails without making filtering your logs difficult
Rails' default logger prefixes each log entry with timestamp and tags (like request ID).
For multi-line entries, only the first line is prefixed which can give you a hard time when grepping logs.
Example
Rails.logger.info(<<~TEXT)
Response from example.com:
Status: 200
Body: It works!
TEXT
With that, the following is written to your log file.
I, [2024-10-04T08:12:16.576463 #1917250] INFO -- : [97e45eae-a220-412d-96ad-e9e148ead71d] Response from example.com:
Status: 200
Body: It works!
If you then run `grep...
How to: Benchmark an Active Record query with a Ruby script
Recently I needed to benchmark an Active Record query for performance measurements. I wrote a small script that runs each query to benchmark 100 times and calculates the 95th percentile.
Note: The script requires sudo permissions to drop RAM cache of PostgreSQL. Due to the number of iterations it was impractical to enter my user password that often. And I temporary edited my /etc/sudoers
to not ask for the sudo password with johndoe ALL=(ALL) NOPASSWD: ALL
.
# Run this script with e.g. `rails ru...
Run all RSpec tests edited or added in the current branch
With this command you can run all the spec files which have been edited or added in the current branch since master:
git diff --name-only master -- ./spec | xargs -I{} rspec {}
- If you have several spec folders add them for path parameter after
./spec
accordingly. - The option
-I{}
creates a placeholder to be replaced. - You can also compare edited/added specs between commits with
<commit>..<commit>
Jasmine: Use `throwUnless` for testing-library's `waitFor`
testing-library are widely used testing utilities libraries for javascript dependent frontend testing. The main utilities provided are query methods, user interactions, dom expectations and interacting with components of several frontend frameworks, which allows us to worry less about the details happening in the browser and focus more on user centric tests instead!
Some of the time you will find a necessity to use methods like [waitFor
](https://testing-library.com/docs/dom-testing-library/api-async/...
Jasmine: Dealing with randomness
Whenever you have to deal with randomness in a jasmine test there are some spy strategies to help you out!
Let's say we have a method Random.shuffle(array)
to shuffle an array randomly and a class that uses shuffle within the constructor.
returnValue
& returnValues
it('shuffles the array', () => {
spyOn(Random, 'shuffle').and.returnValue([3, 2, 1])
array = [1, 2, 3]
testedClass = new testedClass(array)
expect(Random.shuffle).toHaveBeenCalled()
expect(testedClass.array).toEqual([3, 2, 1])
})
If you have...
JavaScript: Listening to a class getting added
Reacting on a class getting added can be done with a mutation observer. Example:
const items = document.querySelectorAll('.item')
const expectedClass = 'active'
const activeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.target.classList.contains(expectedClass) {
// Do something
}
})
})
items.forEach(item => activeObserver.observe(item, { attributes: true, attributeFilter: ['class'] }))
Note that this is not a generic solution – it makes a few assumptions to simplif...
How to ask a (mobile) browser about the true visual viewport
The Visual Viewport API enables developers to access the actually visible area of the page. This differs from the normal viewport if:
- the user has pinch-zoomed
- the on-screen keyboard is visible
- there are other page-independent artifacts
Obtain a VisualViewport
from window.visualViewport
. The object has the properties offsetLeft
and offsetTop
, and three events: resize
, scroll
, scrollend
. You can use these to place and keep an element within the visual vi...
Avoiding Test-Case Permutation Blowout - Steven Hicks
Sometimes you want to write a test for a business rule that's based on multiple variables. In your goal to cover the rule thoroughly, you start writing tests for each permutation of all variables. Quickly it blows up into something unsustainable. With n variables for the business rule, you get 2n permutations/test cases. This is manageable with 2 variables (4 test cases), but at 3 variables (8 test cases) it becomes ridiculous, and anything beyond that feels immediately uncomfortable.
I've noticed myself using an alternate pattern for...
Compare library versions as "Gem::Version" instances, not as strings
Sometimes we have to write code that behaves differently based on the version of a specific gem or the Ruby Version itself. The version comparison can often be seen with simple string comparison like so.
# ❌ Not recommended
if Rails.version > '6.1.7.8' || RUBY_VERSION > '3.1.4'
raise Error, 'please check if the monkey patch below is still needed'
end
If you are lucky, the version comparison above works by coincidence. But chances are that you are not: For example, Rails version 6.1.10.8
would not raise an error in the code ...
RubyMine: Adjust Code Templates
tl;dr
To adjust code templates in RubyMine, navigate to Settings -> Editor -> File and Code Templates.
Example
You can navigate to the test file of the currently open file with the shortcut Ctrl + T
. If no test file exists, you can generate a new test file. If you are not pleased with the output, you can adjust the related code template. To do this, open the project settings and navigate to Editor -> File and Code Templates -> Files. Now, choose your relevant file type and adjust the code template according ...
How to speed up JSON rendering with Rails
I was recently asked to optimize the response time of a notoriously slow JSON API endpoint that was backed by a Rails application.
While every existing app will have different performance bottlenecks and optimizing them is a rabbit hole of arbitrary depth, I'd like to demonstrate a few techniques which could help reaching actual improvements.
The baseline
The data flow examined in this card are based on an example barebone rails app, which can be used to reproduce the r...
How to query GraphQL APIs with Ruby
While most Rails Apps are tied to at least one external REST API, machine-to-machine communication via GraphQL is less commonly seen. In this card, I'd like to give a quick intro on how to query a given GraphQL API - without adding any additional library to your existing app.
Core aspects of GraphQL
Interacting with GraphQL feels a bit like querying a local database. You are submitting queries to fetch data in a given structure (like SELECT in SQL) or mutations to alter the database (similar to POST/PUT/DELETE in REST). You can ...
JavaScript: Humanizing durations
Modern JavaScript includes Intl.NumberFormat to format numbers in different formats and locales.
In this card, we describe a wrapper for it that humanizes a given number of seconds in the "next best" unit, like seconds, minutes, etc.
Example usage
>> new Duration(42).humanized()
=> '42 Sekunden'
>> new Duration(123456).humanized()
=> '1 Tag'
>> new Duration(123456).humanized('es')
=> '1 día'
Code
Here is the code ...
Ruby: Retrieving and processing files via Selenium and JavaScript
This card shows an uncommon way to retrieve a file using selenium where JavaScript is used to return a binary data array to Ruby code.
The following code example retrieves a PDF but the approach also works for other file types.
require "selenium-webdriver"
selenium_driver = Selenium::WebDriver.for :chrome
selenium_driver.navigate.to('https://example.com')
link_to_pdf = 'https://blobs.example.com/random-pdf'
binary_data_array = selenium_driver.execute_script(<<-JS, link_to_pdf)
const response = await fetch(arguments[0])
if (!r...
Simple debounce in vanilla JavaScript
Debouncing a method call delays its execution until after a specified time has passed.
If it's called again before that time has passed, its execution is delayed again.
This technique is commonly used to improve performance when code would be run more often than it needs to.
One example for that are scroll
event handlers in JavaScript: You want to react to a user scrolling, but it's enough to do that when they have stopped scrolling.
Here is a small JavaScript function that you can use for that:
function debounce(callback...
Ruby: How to make your ruby library configurable
You might know a few examples, where you configure some library via a block. One example is the Rails configuration:
Rails.application.configure do |config|
config.enable_reloading = false
end
This card describes a simple example on how to make your ruby library configurable.
Example
module FooClient
class Client
class_attribute :config
def self.configure
self.config ||= Configuration.new
yield(config)
end
def test
uri = URI.parse(FooClient::Client.config.endpoint)
Net:...
Rails: Example on how to extract domain independent code from the `app/models` folder to the `lib/` folder
This cards describes an example with a Github Client on how to keep your Rails application more maintainable by extracting domain independent code from the app/models
folder to the lib/
folder. The approach is applicable to arbitrary scenarios and not limited to API clients.
Example
Let's say we have a Rails application that synchronizes its users with the Github API:
.
└── app
└── models
├── user
│ ├── github_client.rb
│ └── sychronizer.rb
└── user.rb
In this example the app folder ...
Chrome: disable "Choose your search engine" popup in tests
Fresh Chrome installations now show a "Choose your search engine" popup in Europe. This might make your Cucumber tests fail.
Fortunately there is a flag to disable the popup. Add the following option to your chromedriver setup code:
options.add_argument('--disable-search-engine-choice-screen')
I found this flag in Peter Beverloo's list.
Background: This was experienced locally with google-chrome 127.0.6533.72
. In CI I did not get the popup.