Welcome back to Riding Rails, this is Part 2. In this installment, we’ll talk more about models and introduce testing. In case you missed it, check out Part 1 as this builds on what we started.
We left off last time learning about migrations. We just scratched the surface with models and used the Rails model generator to create it and supporting files.
Testing Within Rails
Now that you know about migrations, we’ll get back to working with our model. We’re going to use test-driven development to work on our model, and because you’re a Ruby developer now, we test our code.
By default, Rails is set up for Test Driven Development (TDD), not the common alternative Behavior Driven Development (BDD). Tests in Rails inherit from
ActiveSupport::TestCase, which in turn inherits from
Minitest::Test. Minitest also has a
Minitest::Spec option available, but to use BDD syntax like
it in Rails, we would have to install a gem. If we were going to do that, I would have installed RSpec (my preferred spec library) before generating the files. If we were using RSpec, then we would see
*_spec.rb files instead of
*_test.rb files. It’s not a big deal to install the gem, but I’m trying to keep extraneous installs to a minimum to avoid confusion. Thus we’ll use TDD. If you aren’t familiar with BDD, there are specs in place of tests. So I apologize in advance if I call something a “spec” instead of a “test!”
If we were using
Minitest::Test directly, we would have to write tests with underscores for the method name like so:
ActiveSupport::TestCase running on top of
Minitest::Test, we can write the same test like this:
The key takeaway is that with
ActiveSupport::TestCase we can use the
test method and pass it a string, rather than being forced to provide a Ruby “test class friendly” name. This will allow us to be more descriptive with our test case names.
test/models/job_test.rb, and let’s write our first test.
You’ll notice that in
job_test.rb there is a class defined as we saw in the migration. There is also an
include and some comments in the class. Go ahead and delete the comments (the lines that start with a
#). That leaves us with the class definition and the include. We’ll use this included file (
test_helper.rb) in all of our tests. So if we need to write any custom logic to be commonly shared across all of our tests, it can go in here.
Now let’s write our first failing test. We’re going to assert one requirement per test. This is something I learned long ago by reading “The Art of Unit Testing” by Roy Osherove. If you are not familiar with the concepts of writing unit tests, such as what “assertions” are and the important role they play in unit testing, I highly recommend reading that book. Let’s start with validations. We want to ensure that the
Job object has a
title, otherwise it should not be allowed to save. You’ll see in the test below that we create a new
Job object without a title, and then tell the test method, using
assert_not, that we expect the job to NOT be saved.
Now that we have our test, let’s run it with the command
rails test. You’ll see it fails as expected:
F indicates a failure, and you can see the message
Expected true to be nil or false. That’s not very specific. Let’s fix that by passing a message to the
Now, when you run the test again, we have a more meaningful failure message:
With the failing test, we’ll want to fix it by adding validations to the
Job class. Open up
models/job.rb and fix it by matching the class below:
Are you surprised by the
Jobclass? Where are our attributes we defined in the last part? The answer lies in
app/models/application_record.rb, see how it inherits from
ActiveRecord::Base? Well, ActiveRecord, Rails’ default ORM (object-relational mapping), does the mapping from the database for us. Seems almost magical, doesn’t it? Still curious? Read more about ActiveRecord in the docs.
Re-run the test, and now it passes. Let’s do another! We want to make sure more fields are required. Let’s target the metadata that we are storing about a job. In your same test file, underneath the previous test we just made, add the new test code as shown here:
Now that you are familiar with the syntax from our first test, you should be able to see that this second test expects that the
source attribute needs to have a value in order for the Job to save successfully. Since we are providing a null value to
source this test should fail. Run the failing test using
rails test and… wait… what? It passed?! Hmm, that’s not right. Do you see the problem? We are specifying
source: nil, but the now required
title isn’t set, so THAT is causing our Job to not save even though we haven’t added validations to the
source attribute yet. I guess we can add the
title with a value to the hash we are sending to
Job.new, but that’s going to get old fast when we add more validation of attributes. “There must be a better way,” I can hear you exclaiming. You are right!
There are a few ways to tackle this problem that we have. Rails provides us with Fixtures, which is just a fancy word for “sample data.” We could use these Fixtures in our tests. We could also create a hash of all the attributes needed to create a model and change just the attributes we are interested in for each specification. A third option, which I personally use, is a gem called FactoryBot (previously known as FactoryGirl). That said, let’s use what we have instead of introducing another gem. Plus Factories vs. Fixtures is an another conversation entirely. Let’s go the Fixture route since Rails has already set it up for us. If you hop over into
test/fixtures/jobs.yml, you’ll see some basic code that Rails created for us. Let’s use the attributes it provides to save typing. Change the file to look like this:
If you look at
test/test_helper.rb, you’ll notice that it is already set up to load all of our fixtures before each test:
With that in place, let’s change our two tests to the following:
As you can see, we are initially loading up our test data from the Fixture, and then nullifying the field that we want to test. Rerun the tests using the command
rails test, and uh oh!
Since we’re going along with actual development, this could happen. Let’s take a closer look at the error:
It looks like we used a reserved word in ActiveRecord,
type in our
job model. Now, we could go and change this, but
job.type sounds a lot better than
job.job_type or something. ActiveRecord reserves the word
type for doing single table inheritance (STI). We can change this, telling ActiveRecord that we’re not going to be using STI with our
job model. Open the model file (
app/models/job.rb) and inside the class add the following line:
Save, and re-run the tests. The error goes away, and we’re left with what we expected back when we started this second test, a failure. Let’s fix that the same as we did with the first test we wrote; you’re
job model class should now look like this:
Run the tests again (
rails test) and voilà, success!
Other Validation Helpers
Now that you know about the
presence validator, we can add this to other attributes. But what about other types of validation in Rails? There are a ton of other built-in helpers available. Let’s say we want to make sure that the
type attribute must contain only certain values like “Full Time”, “Part Time”, and “Contract”. To add such a validation we can utilize the inclusion validator which lets us specify a set of valid values for the attribute. Let’s test that out now. Remember we are using TDD so let’s write the test first. Add the following to your test file below the second test we just finished.
You know the drill by now, run the test making sure it fails. Now let’s fix it, add this to your
Re-test, and it works!
Save, Test, Rinse, Repeat
We’ve repeated saving and running the tests a bunch of times now. You’re probably a bit annoyed having to repeat these steps over and over. The good news is that there is a better way. It involves installing a gem (well 2), but that’s a simple task in Rails thanks to Bundler. I thought about omitting this since it’s a separate install, but this isn’t new syntax that you need to learn or anything. It’s a merely an automated test runner. It will make your development process much better.
Open up your
Gemfile and add we will add two gems in the
group :development, :test, like so:
Now, from a terminal window aimed at your project root, run the command below to install the two new gems we just added:
Once that’s completed, initialize
guard-minitest from the same terminal with this command:
This creates a
guard what to run. Open the
Guardfile in the root of your project and uncomment the section that starts with
# Rails 4. Finally, where it says
guard :minitest do change that to read
guard :minitest, spring: "bin/rails test" do. If done correctly your
Guardfile should look like this:
Open up a new Terminal tab/window in the same application root folder. Then fire up guard by just typing
guard. If it all worked correctly, you should see
3 runs, 3 assertions, 0 failures, 0 errors, 0 skips. Every time we save from now on,
rails test will be run for us. Much easier!
Trying Out Guard
Since this is the first time you’re using Guard, let’s make sure it’s working as expected. Add a new test inside
job_test.rb. You are familiar with the process so go ahead and make sure that the
source_url is provided, and here’s something new, make sure that it’s actually a URL. There are various ways to enforce that, and there isn’t a wrong way providing you really test it out. Save the test(s), and Guard will run it. Fix the failure and Guard will jump into action and rerun the test. Continuous feedback from Guard makes TDD/BDD very productive. When you’re done with testing the new requirements, you should have at least five total tests. Remember, one assertion/unit of work per test. I’ll leave it to you to decide how many tests you add depending on the solution you choose. Don’t shy away from any option, this is how we learn.
In this post, we only validated a few of the attributes in our model. Look at what we have and where it can be improved. Even though users won’t be entering data into the database directly, remember the mantra of “garbage in, garbage out.” We don’t want just anything entered for say the
company_url, do we? If you need a refresher, just have a look at Figure 2 under the heading Models. You could potentially require every attribute in our
job model. However, this may limit what we’re able to store from other sources. However, if you plan to stick to just one source (GitHub Jobs), then that would probably be beneficial. Don’t forget to test out any validations that you add.
In this second part of the tutorial, we saw how we could test-drive requirements with models. We learned that Rails comes with Fixtures, what they are, and how they can be used in our tests. After numerous times of saving and running the tests manually, we discovered that Guard is a fantastic tool that will give us constant, automated feedback.
Look out for part 3 of this series, in which we will discuss moving our application forward. With our
job model in place, we’ll start using the model when we are querying GitHub Jobs and persisting everything to our database. I hope that you found the information here useful. If you have any questions, please leave a comment below or you can contact us.