Project maintenance: four levels of code quality
Code quality can be measured in four levels:
- (Working code)
- Reliable code (minimum)
- Readable code (ok for short-lived code)
- Changeable code (standard level)
The code quality of a project directly impacts its maintainability.
Generally you should aim for level 3. If the code will stay for less than a few months, it may stay at level 2. Never go below level 1.
0. Working code
You have implemented that feature and it works. Congrats! You have reached level zero, which means three levels of code quality lie ahead.
First, m...
Use DatabaseCleaner with multiple test databases
There is a way to use multiple databases in Rails.
You may have asked yourself how you're able to keep your test databases clean, if you're running multiple databases with full read and write access at the same time. This is especially useful when migrating old/existing databases into a new(er) one.
Your database.yml may look like this:
default: &default
adapter: postgresql
encoding: unicode
username: <%= ENV['DATABASE_USER'] %>
host: <%= ENV['DATABASE...
How to bind an event listener only once with Unpoly
You can use Unpoly's up.on with a named listener function and immediately unbind this event listener with { once: true }:
up.on('up:fragment:inserted', { once: true }, function () { ... })
In Unpoly 1 you can immediately unregister the listener with up.off:
up.on('up:fragment:inserted', function fragmentInsertedCallback() {
up.off('up:fragment:inserted', fragmentInsertedCallback)
// ... code for the callback function, which should run only once
})
Exam...
Chrome DevTools: List Registered Event Listeners
In Chrome DevTools you can use getEventListeners(object) to get a list of registered event listeners on the specified object.
You can also do this without the console, by selecting an element in the DOM inspector. In the element details, select the tab Event Listeners".
Example
const registry = getEventListeners(document)
registry['up-click']
// 0: { useCapture: false, passive: false, once: false, type: 'up:click', listener: ƒ }
// 1: { useCapture: false, passive: false, once: false, type: 'up:click', listener: ƒ }
//...
How to get information about a gem (via CLI or at runtime from Ruby)
When you need information about a gem (like version(s) or install path(s)), you can use the gem binary from the command line, or the Gem API inside a ruby process at runtime.
gem binary (in a terminal)
You can get some information about a gem by running gem info <gem name> in your terminal.
Example:
$ gem info irb
*** LOCAL GEMS ***
irb (1.4.1, 1.3.5)
Author: Keiju ISHITSUKA
Homepage: https://github.com/ruby/irb
Licenses: Ruby, BSD-2-Clause
Installed at (1.4.1): /home/arne/.rbenv/versions/3.0.3/lib/ruby/g...
JavaScript: Working with Query Parameters
tl;dr: Use the URLSearchParams API to make your live easier if you want to get or manipulate query parameters (URL parameters).
URLSearchParams API
The URLSearchParams API is supported in all major browsers except IE 11.
It offers you a bunch of useful methods:
-
URLSearchParams.append()- appends a query parameter -
URLSearchParams.delete()- deletes the specified query parameter -
URLSearchParams.get()- returns the value of the specified query parameter - `URLSearchP...
Jasmine: Fixing common errors during initialization
Due to the way we setup Jasmine tests in our projects, you may run into various errors when Jasmine boots.
Setting jasmineRequire on undefined
Jasmine 4 may fail with an error like this:
Uncaught TypeError: Cannot set properties of undefined (setting 'jasmineRequire')
This is due to issues in Jasmine's [environment detection](https://github.com/jasmine/jasmine/blob/502cb24bb89212917a3c943b593fd918ffc481cb/lib/jasmine-core/...
Generating an Entity Relationship Diagram for your Rails application
This card explains how to generate an entity relationship diagram for your Rails application.
We also show how to limit your ERD to a subset of models, e.g. models inside a namespace.
Generating a full ERD
Option A: RubyMine
- Right-click anywhere in your project tree
- In the context menu, find the "Diagrams" menu item at/near the bottom
- Inside, choose "Show diagram" → "Rails Model Dependency Diagram"
- A new tab will open with the diagram inside. You can modify it there, and export it as an image.
Option B: Use rails-e...
Version 5 of the Ruby Redis gem removes Redis.current
Redis.current will be removed without replacement in redis-rb 5.0.
Version 4.6.0 adds deprecation warnings for Redis.current and Redis.current=:
`Redis.current=` is deprecated and will be removed in 5.0.
If your application still uses Redis.current, you can only fix it by no longer using it. Here is how.
Redis.new when you need it
You can easily instantiate a Redis client when you need it.
There is probably already a constant like REDIS_URL that you use to configure Sidekiq or similar. So just use that one.
``...
How to access Chrome Devtools when running JavaScript tests via CLI
While we are used to run our JavaScript tests on a test page within our Browser, it's also possible to run them on the command line with NodeJS. I think that's actually the most common way to run JS tests.
Given a Vue project that uses Jest (via vue-cli-service) with the following package.json:
{
"scripts": {
"test": "vue-cli-service test:unit --testMatch='**/tests/**/*.test.js' --watch"
},
}
This allows us to run J...
Jest: How to clear the cache
I recently was in a weird situation where my (Jest/CLI) tests were referencing a function that was no longer part of my code - I had just refactored it.
Apparently Jest has some kind of cache that caused the issue, running npx jest --clearCache solved it for me.
RSpec: You can super into parent "let" definitions
RSpec's let allows you to super into "outside" definitions, in parent contexts.
Example:
describe '#save' do
subject { described_class.new(attributes) }
let(:attributes) { title: 'Example', user: create(:user) }
it 'saves' do
expect(subject.save).to eq(true)
end
context 'when trying to set a disallowed title' do
let(:attributes) { super().merge(title: 'Hello') } # <==
it 'will not save' do
expect(subject.save).to eq(false)
end
end
end
I suggest you don't make a habit of using this regula...
Integrating ESLint
Introduction
To ensure a consistent code style for JavaScript code, we use ESLint. The workflow is similar to integrating rubocop for Ruby code.
1. Adding the gem to an existing code base
You can add the following lines to your package.json under devDependencies:
"devDependencies": {
"@eslint/js": "x",
"@stylistic/eslint-plugin": "x",
"eslint": "x",
"eslint-plugin-import": "x",
"globals": "x",
}
...
How to add esbuild to the rails asset pipeline
This are the steps I needed to do to add esbuild to an application that used the vanilla rails asset pipeline with sprockets before.
Preparations
- update Sprockets to version 4
- add a
.nvmrcwith your preferred node version (and install it) - add gems
jsbundling-railsandforemanto yourGemfile:gem 'jsbundling-rails' group :development, :test do gem 'foreman' # ... end bundle install- run
bin/rails javascript:install:esbuildin a console to prepare esbuild. - run `yarn instal...
PostgreSQL: "WHERE NOT <column> = '<value>'" statements do not include NULL values
Sometimes we write plain SQL queries in migrations so we don't have to mock ActiveRecord classes. These two migrations do the same:
class Migration1 < ActiveRecord::Migration[5.2]
class User < ActiveRecord::Base; end
def up
add_column :users, :trashed, :boolean
User.update_all(trashed: false)
end
end
class Migration2 < ActiveRecord::Migration[5.2]
def up
add_column :users, :trashed, :boolean
update("UPDATE users SET trashed = #{quoted_false}")
end
end
The plain SQL migration is less code, but h...
esbuild: Make your Rails application show build errors
Building application assets with esbuild is the new way to do it, and it's great, especially in combination with Sprockets (or Propshaft on Rails 7).
You might be missing some convenience features, though.
Here we cover one specific issue:
Once you have started your development Rails server and esbuild with the --watch option (if you used jsbundling-rails to set up, you probably use bin/dev), esbuild will recompile your assets upon change, but build errors will only be printed to the terminal. Your application won't complain about them ...
Carrierwave: How to attach files in tests
Attaching files to a field that is handled by Carrierwave uploaders (or maybe any other attachment solution for Rails) in tests allows different approaches. Here is a short summary of the most common methods.
You might also be interested in this card if you see the following error in your test environment:
CarrierWave::FormNotMultipart:
You tried to assign a String or a Pathname to an uploader, for security reasons, this is not allowed.
If this is a file upload, please check that your upload form is multipart encoded.
Factor...
Deployment: Merge consecutive commits without cherry-picking
You want to deploy new features but the latest commits are not ready for production? Then use git merge master~n to skip the n-last commits.
Tip
A big advantage of merging vs. cherry-picking is that cherry-picking will create copies of all picked commits. When you eventually do merge the branch after cherry-picking, you will have duplicate commit messages in your history.
Example
It's time for a production deployment!
git log --pretty=format:"%h - %s" --reverse origin/production..origin/master
0e6ab39f - Feature A
6396...
Capybara: Working with invisible elements
When Capybara locates elements in the DOM, by default it allows only accessing visible elements -- when you are using a driver that supports it (e.g. Selenium, not the default Rack::Test driver).
Consider the following HTML:
<div class="test1">One<div>
<div class="test2">Two</div>
With some CSS:
.test1 { display: block }
.test2 { display: none }
We will be using Capybara's find below, but this applies to any Capybara finder methods.
Default: visible: :visible
As described above, by default Capybara finds ...
Using ngrok for exposing your development server to the internet
Sometimes you need to access a dev server running on localhost from another machine that is not part of the same network. Maybe you want to use your phone to test a web page, but are only in a guest WiFi. In the past, we often used some port forwarding or other techniques to expose the service to the internet.
Enter ngrok, a command line tool that gives you an on-the-fly internet...
RSpec: Inferring spec type from file location
RSpec Rails can automatically mix in different behaviors to your tests based on their type tag, for example enabling you to call get and
post in specs with the tag type: :request.
Alternatively you can skip these tags by setting the config config.infer_spec_type_from_file_location! in the spec_helper.rb. This will automatically choose the right type context based on the file location of the test.
For instan...
Rails 6.1: where.not changes behaviour from NOR to NAND
Since Rails 6.1, if we use where.not with multiple attributes, it applies logical NAND (NOT(A) OR NOT(B)) instead of NOR (NOT(A) AND NOT(B)). If you do not take care, this change will increase the matched set.
Examples
"Don't send newsletters neither to admins nor to trashed users!" becomes "Don't send newsletters to trashed admins".
User.where.not(role: 'admin', trashed: true)
# Before Rails 6.1, with NOR
=> "SELECT "users".* FROM "users" WHERE "users"."role" != 'admin' AND "users"."trashed" != TRUE"
# Equivale...
RSpec: Defining negated matchers
You can use RSpec::Matchers.define_negated_matcher to define a negated version of an existing matcher. This is particularly useful in composed matcher expressions or to create more expressive and meaningful matchers.
You should only negate "atomic" matchers with an unambiguous inversion. For example, have_radio_button(label) can be negated, whereas have_active_radio_button(label) should not be. Its inversion could either mean "there is a radio button with that label, but it is not active", or "there is no radio button with that la...
Jasmine: Spy on value properties
Jasmine has spyOnProperty(), but it only works if the property is implemented using getter and setter functions. This is a known limitation of Jasmine.
If the mocked property is a simple value, it will not work:
const x = { foo: 1 }
console.log(x.foo) // 1
spyOnProperty(x, 'foo').and.returnValue(2)
// Throws: Error: <spyOnProperty> : Property foo does not have access type get
Below you can find a function `spyOnValuePr...