I recently had another code review with Edward Anderson. He wen’t over my model specs, and had several critiques that helped me understand how to write stronger tests. He also brought me more up to speed with how to write specs for my scopes. Scopes can be thought of as very similar to Ruby class methods.
Take a peak at what the Rails guide says about scopes:
Scoping allows you to specify commonly-used queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as
includes. All scope methods will return an
ActiveRecord::Relationobject which will allow for further methods (such as other scopes) to be called on it.
Below is an example of my event_spec.rb file before my code review:
This looked passable to me before my AirPair with Edward, but he shared a few tips to help me spot the difference.
I learned that there are 3 parts to any spec. He mentioned that the steps aren’t always as strictly delineated as this, but that every spec will contain all 3 of the following steps.
- The setup: This can be seen on lines 5, 12, 19, etc… in the first example and where we use FactoryGirl to setup our object.
- The execution: This is the
expect(argument)portion of lines 6, 8, 13, 15, etc… This is where we call some code to assert our step 3.
- Assert our desired result: This looks like the the be_valid on line 6, the be_present on line 8, and so on. When he said assert our desired result, I take it to mean, what we know should make our test pass or fail. The
be_validmethod passes a
.valid?method to our expect argument. On line 6 that would be the argument
event, which is the local variable our FactoryGirl object is stored in within that current spec.
Now that we have an idea of the components of a spec file, lets get into how Edward helped me remove, or edit some weak-sauced specs that I wrote.
If you see throughout the entire file in the first example, I have done an ok job of my object setups, which basically says that if my required attributes are nil, then the object, should not save because it is invalid. This is because of the validations, created in the Event.rb model.
Lets have a quick look before moving forward:
On line 6, the
validates_presence_of :author, :name, :title, :event_on, :type is what will not allow our object to save, if any of the aforementioned attributes are blank or nil values in our params hash:
We are only checking that the value assigned to event.author on line 4, is present. Edward skillfully explained to me that the method
be_present only asserts that
expect(event.author).to is not
nil. I’m very glad he broke down the meaning of some Rspec matcher methods, because my thinking before our code review was that if I had a test for an
event.author string, and a user entered in a value other than
"Jimmy Dean" that, it wouldn’t
be_valid because when I ran that through the terminal, it wasn’t passing.
I realized though that when I am assigning the string
"Jimmy Dean", that calling be_valid instead of be_present is calling the
.valid? method on any value entered by a user. In other words, the client value, should match the value thats actually stored, which is an operation available to me through Ruby’s assignment
= operator.. Definitely not something I should be testing for.
Here’s what my final event_spec.rb file looks like after some professional guidance:
Oh, what are those scopes tests at the bottom you ask? It’s time for some scope specs.
Since I had been digesting Edward’s steps to a spec, I had a lot more confidence in the setup of all of these scope specs. Since we already know that scopes aid us in specifying commonly used queries, and we have an idea of the event.rb data-model, then we can just follow the script. Setup our objects, execute the spec case method we’re testing for, and make an assertion that will provide our desired results.
Taking the first scope spec
Event.upcoming_events, we’ll have a quick walk-thru.
First off, Edward encouraged me to always create groups for similar code behavior. The
describe "scopes" do block on line 39 in the above example works well for such a task.
our first spec in our
describe ".upcoming_events" block is making assertions that ensure that a query for an upcoming event will only pull records from up to 3 days ahead of the current time of query.
We set up our cases, which is an object stored into
future_events and another one stored into
past_events. Notice that our
future_events has an
2.days.from_now, while our
past_events has an
1.day.ago. Can you guess which one shouldn’t be in our
So now for us to call out the method that we wrote this spec for.
expect(future_events.event_on).not_to, are establishing the boundaries of the intent of this particular spec so that we should only receive events from up to 3 days in the future.
Our first assertion says that
future_events is expected to
be > (be greater than)
1.minute.from_now. If we ever get mixed up on what
from_now means relative to the current time. We can just open up IRB or PRY in our terminal and type Time.now a few times.
Here I’ll show what happens when I do it:
The illustration above is just a visual way of saying that
1.minute.from_now is relative to when the query is executed. Could be right now, or 1 minute from thanksgiving 2020 at midnight, if thats when the query was executed.
There are a host of other things I learned in my last pair with Edward, like some new multi-line comment slight-of-hand tricks, but you should really checkout our pair and consider asking him to share an hour of his time. You’ll most certainly come out of that hour with your brain a little sore, and a huge smile on your face from the educational workout he consistently brings to the table.
Here’s our most recent pair: