- Day 1: From Nothing to Something
- Day 2: More Things about Relations, Tests, etc.
- Day 3: My Thoughts on Ruby
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.
-
first_name
andlast_name
must have values. -
gender
must be one of male, female and others. -
age
must be a natural number. -
address
field is an object and only the three keyscountry
,address_1
andaddress_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 theabc
refers to a variable or the functionabc()
is called. - When calling the function
foo a=> 1, b => 2, c => 3
how many arguments doesfoo
have when written in this way? The answer is that we don't know, we need to see the signature offoo
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.