Building Ruby on Rails from Scratch, Day 2
Tags: programming, tutorial, ruby, rails
Published: 2022-07-25 01:39:00

![https://dev.to/rly](https://i.imgur.com/mtgKdGS.png)

- Day 1: [From Nothing to Something](https://lazypro.medium.com/building-ruby-on-rails-from-scratch-b855584ff4f8)
- Day 2: [More Things about Relations, Tests, etc.](https://lazypro.medium.com/building-ruby-on-rails-from-scratch-day-2-d609fce59811)
- Day 3: [My Thoughts on Ruby](https://lazypro.medium.com/ae31c975bf32)

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](https://github.com/wirelessr/Hello-RoR/commit/d34ff4f28f4c3b7175d66ad5d52cb7494359df72).

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](https://github.com/wirelessr/Hello-RoR/commit/59f5bfa40abcf7d6e93f623a568f53b6b1a73ac5).

## 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](https://github.com/wirelessr/Hello-RoR/commit/2f71665acfac2d6765b374e1301b2045f8377cf6). 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](https://github.com/wirelessr/Hello-RoR/commit/bb50fed7e11a73d6ced7249e48197321d2240fe7) 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](https://www.betterspecs.org/).

## 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.