Building Ruby on Rails from Scratch, Day 2

More Things about Relations, Tests, etc.

Chunting Wu
5 min readJul 25, 2022
https://dev.to/rly

In the previous article, our application has almost finished building the user model. In this article, we will proceed to build a model of each user’s corresponding store, i.e., one-to-one mapping.

In addition, user-related unit tests will be created to practice RSpec.

So, let’s get started.

Establish a one-to-one mapping

First of all, the same store model is created by scaffold, but the difference is that the store must be assigned to a user at the end.

$ bin/rails generate scaffold shop name user:belongs_to

In this way, the controller and routes of stores will be created. But this is not enough, because it is a one-to-one mapping relationship, so we must also modify the original user model, as follows Github commit.

It has to be specified as has_one in app/models/user.rb, otherwise it becomes a one-to-many mapping. In addition, Rails has an inborn problem when dealing with mapping relationships, which is N + 1 queries.

In this example, N + 1 queries is not a serious impact because it is a one-to-one mapping, but if it is a one-to-many mapping, then it will have a performance impact. The solution is also very simple, and is provided in the above mentioned commit.

Change the original Shop.all generated by scaffold to Shop.includes(:user). Then the user model will not be searched for one record at a time, but will be listed at once.

Create nested routing

Since users and stores have an ownership relationship, we can also create a nested route to get the stores under users.

Just modify config/routes.rb to add the store resources under the user’s resources, as shown in the Github commit.

Setup unit tests

Nowadays the most popular test framework on Rails is RSpec, so I also did some exercises with RSpec.

The installation is very simple, just add a new line gem 'rspec-rails' at Gemfile, and it is recommended to add it under the development and test groups. Next, install and configure.

$ bundle install
$ bin/rails generate rspec:install

We have generated various models by scaffold before, so we continue to generate corresponding tests by scaffold as well. Let’s take the user model as an example.

$ bin/rails generate rspec:scaffold user

Normally, all test cases created can be executed directly at this time.

$ rspec

However, on the M1 system, we will encounter problems.

Rails bootstrap puts selenium-webdriver in by default, but selenium-webdriver is compiled for the x86 platform, so an error occurs when running rspec.

I didn’t have to run end-to-end tests, so I just unplugged the irrelevant dependencies from Gemfile and rspec ran correctly.

The result of this section is as follows, Github commit. From the commit, you can see that scaffold generates many user model related test cases.

Start testing

Let’s test the model first. There are several constraints that must be tested.

  1. first_name and last_name must have values.
  2. gender must be one of male, female and others.
  3. age must be a natural number.
  4. address field is an object and only the three keys country, address_1 and address_2 are allowed.

The full test is listed in the link below.
https://github.com/wirelessr/Hello-RoR/commit/55fca06899d60256345c9d5ec5f14a2aa51a8b3f

Then we proceeded to test the controller. Fortunately, rspec:scaffold already has the basic framework set up, so we just need to fill in the details.

The native test file is in spec/requests/users_spec.rb, and there are a few places where the skip function skips, and all we have to do is follow the instructions in skip to replace it with a legal or illegal user model.

The Github commit is attached.

My reflections on RSpec

In fact, the syntax of RSpec is very familiar to me because of my experience in writing mocha on top of Node. Whether it’s describe or it or even expect, it’s all similar, so I didn’t encounter any difficulties.

In the process of practicing, I referred to an RSpec best practice document. I used to write mocha very casually, without referring to any rules, so this document also provides good advice. In the future, whether it’s Ruby’s RSpec or Node’s mocha, it should be able to be written in a more beautiful way.

The reference link is directly attached, Better Specs.

My reflections on Ruby

After practicing Rails for a few days, I didn’t encounter too many difficulties because I could see more or less the shadow of other languages in Ruby, among which I think the closest should be Python, and many concepts are shared.

However, there are some Ruby-specific behaviors that took me a while to study.

Symbol

It is common to see variables starting with a colon in Ruby, such as :abc, which actually refers to the string abc. Unlike a string, however, this string is immutable.

This has the advantage that the comparison between symbols does not need to be done character by character, but can be done directly to the memory address, so it is faster than a string comparison.

This concept is also found in Python, which places common words and numbers at fixed memory addresses to speed up references. In the Python example below, the addresses of a and b are the same, and both come from the object of 1, which has been predefined.

>>> a = 1
>>> b = 1
>>> id(a)
5777693336
>>> id(b)
5777693336

Parentheses can be omitted

In Ruby, both parentheses and braces can be omitted, so a bunch of behaviors appear that are difficult for first-time users to understand. For example.

  • When formatting a string, "#{abc} is good", it’s hard to know if the abc refers to a variable or the function abc() is called.
  • When calling the function foo a=> 1, b => 2, c => 3 how many arguments does foo have when written in this way? The answer is that we don’t know, we need to see the signature of foo to know.

There are also various variants, such as this omission of parentheses really made me suffer a lot in reading other people’s code.

Class definition

The definition of an attribute within an object is @, so @abc is equivalent to Python’s self.abc.

But if we define a function using self, it means something completely different, def self.bar(), which in Python terms means bar is a classmethod.

Poor performance

In CPython, the performance of the entire application is limited by the implementation of GIL, and even with multi-threaded execution, all operations must still be performed sequentially.

This is also true for Ruby, which also has GIL, and usually uses pre-fork in order to fully utilize the machine’s resources, and the Gunicorn used in Python is actually derived from Ruby’s Unicorn.

Afterword

Although the process of practicing Rails went smoothly, I actually encountered a lot of difficulties while doing the code review, both related to the Ruby features I mentioned in the previous section, and from the Rails features.

I’ll describe my thoughts on Ruby on Rails in the next article, which should be the last in this series.

--

--

Chunting Wu
Chunting Wu

Written by Chunting Wu

Architect at SHOPLINE. Experienced in system design, backend development, and data engineering.

No responses yet