Select Mode

Starting a Rails App, Part 2

This is the second, and final, post in the series about starting off on a Rails application. In the first post the basis for a “Planets” application was created. I ended up with a database (model) and a functioning mechanism around that database that let me create, edit, and delete records (views). All of that was knitted together into a working application by a controller. However, those views have no validation on them. That means the database is not protected from bad data. I’ll fix that here and then focus a lot on how Rails apps are tested.

Test the Application

At this point I haven’t actually done a lot of specific work with my application. Rails has really generated all the code. My only significant contribution (beyond indicating what a Planet model looks like) was entering some data. From the first post, you know that Rails generated some test stubs along with the other functionality. I suppose this means there’s not much point testing the application yet, right? Well, I’ll try it out:

rake test

Running that should give you no failures and no errors. The set of tests being run here is for the model and controller tests that Rails generated as part of the scaffolding. These are the files I called out in the first post. These test files, as I had indicated, are somewhat minimal at this point so, on the one hand, these passing shouldn’t necessarily fill you with a great deal of confidence but, on the other hand, they are testing the basics of operations. Look at test/controllers/planets_controller_test.rb, for example, and you’ll see at least some substance. Compare that with test/models/planet_test.rb, where you will see considerably less. What this shows you is that the controller is being tested to some degree while the model is not being tested at all.

As you start to craft tests, you can tell Rails that you only want to run a subset of those tests. For example, you could just run tests for your controllers and for your models using these specific commands, respectively:

rake test:models
rake test:controllers

I’m going to come back to testing the application later in this post. In fact, this post is ultimately going to be mostly about testing the application. But first I need something more to test.

Validate the Model

With the application as designed so far, there is literally no validation on the form used to input data about a planet. In fact, you can enter no data at all on the form and an almost entirely empty record will be created in the database and displayed for your viewing pleasure on the planet list page. I say “almost entirely empty” because every table contains an identity column that is called ‘id’. The reason any data (including no data) can be saved is because the model has currently not been given any conditions for data integrity. To add these conditions, I’m going to change my Planet model in app/models/planet.rb:

Now if you try to create a planet and you don’t give a value for the name, diameter or mass then you will be given an error that showcases each individual problem. As you can probably guess, the validates() method calls a validator that Rails provides. That validator will check one or more model fields against one or more conditions. In this case, the condition is specified by “presence: true”, which tells the validator to check that each of the named fields is present and that the contents of that field have some value. If you don’t like adding the condition qualifier, you can use a different validator name:

Personally, I prefer the second approach but the effect is the same either way. Let’s try our tests again, just for giggles:

rake test

Everything passes. So here’s a perfect example of where you don’t trust the tests just because they were generated. Above I essentially changed how data can get into the model and the Rails tests know nothing of that. Further, they clearly don’t do enough to even recognize this change. At first glance, anyway. In truth, the generated Rails tests would find an issue but they are automatically using fixture data, which I’ll come back to later.

Another thing I can is check for whether each planet name that is entered is unique. So I’ll add that condition to my model:

With that in place, assuming I have my seed data (from the previous post) loaded up, if I try to create another planet named “Mars” via the web interface, I’ll be told that “Name has already been taken”. As with the presence validation, you can use a different format if you don’t like qualifying the condition at the end:

Utilize the Console

What you can see here is that the model sort of wraps around your database and, as such, this makes models the place to put validations. What’s important to realize is that these validations work regardless of where the data comes from. Someone trying to enter data in via the web interface or someone trying to inject data in via the database directly will encounter the same validations. I wanted to prove that to myself however and I found a way to do that with the Rails console. So I tried this:

rails console

This console is basically opening up an irb session but with the added benefit of my Rails application environment being loaded into it. That means I can play around with my application code from the console. For example:

$ rails console
Loading development environment (Rails 4.1.1)
irb(main):001:0> planet = Planet.new
=> #

irb(main):002:0> Planet.column_names
=> ["id", "name", "image_url", "details", "facts", "created_at", "updated_at", "diameter", "mass"]

irb(main):003:0> planet.attributes
=> {"id"=>nil, "name"=>nil, "image_url"=>nil, "details"=>nil, "facts"=>nil, "created_at"=>nil, "updated_at"=>nil, "diameter"=>nil, "mass"=>nil}

Here I create a new instance of my Planet model. I check the column names on the class and then I check the attributes on the instance of the class. Specifically, the call to attributes() returns a hash of the attributes that Active Record determined were present by looking at the columns in the table. The reason I’m even doing this is because I want to see if I can attempt to create a planet and insert into my database directly, thereby bypassing the validations. This not only gets me practice with the console but also will tell me what kind of confidence I have in this aspect of Rails operation.

Clearly I can see the attributes of my Planet class so I know I’m getting an instance of my model object. Now — moment of truth! — I’ll try to save it to the database without having entered any values:

irb(main):004:0> planet.save
   (0.0ms)  begin transaction
  Planet Exists (0.0ms)  SELECT  1 AS one FROM "planets"  WHERE "planets"."name" IS NULL LIMIT 1
   (0.0ms)  rollback transaction
=> false

It’s not obvious why it failed but clearly it did. I can check a bit more into that:

irb(main):005:0> planet.errors.any?
=> true

irb(main):006:0> planet.errors.full_messages
=> ["Name can't be blank", "Diameter can't be blank", "Mass can't be blank"]

Here I check if there are errors, which there clearly were, and then I find out what those specific errors were. I can check a specific error for a specific attribute as well:

irb(main):007:0> planet.errors[:name]
=> ["can't be blank"]

So now I’ll try to add some values to the properties so that I can make a valid planet:

irb(main):008:0> planet.name = 'Jupiter'
=> "Jupiter"

irb(main):009:0> planet.diameter = 'Really big.'
=> "Really big."

irb(main):010:0> planet.mass = 'Huge.'
=> "Huge."

Incidentally, this brings up a good point: notice that I can enter any values for diameter or mass. All my validations I put in earlier did was check for the presence of some value, as opposed to a specific type of numeric value. Anyway, now that I’ve filled in the required fields and I’m not using an existing planet name, I’ll save again:

irb(main):011:0> planet.save
   (0.0ms)  begin transaction
  Planet Exists (0.0ms)  SELECT  1 AS one FROM "planets"  WHERE "planets"."name" = 'Jupiter' LIMIT 1
  SQL (0.0ms)  INSERT INTO "planets" ("created_at", "diameter", "mass", "name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2014-05-25 18:38:39.325070"], ["diameter", 0.0], ["mass", 0.0], ["name", "Jupiter"], ["updated_at", "2014-05-25 18:38:39.325070"]]
   (15.6ms)  commit transaction
=> true

Cool! Looks like it worked. I confirm that by checking out my interface in the browser and, sure enough, I have a new entry for Jupiter. I can also check that here in the console:

irb(main):012:0> Planet.count
   (0.0ms)  SELECT COUNT(*) FROM "planets"
=> 1

So what this showed me is that the validations do serve to provide data integrity regardless of how the data is getting to the database. Somewhat peripherally, this also confirmed to me that behind the scenes my Ruby code is being translated down into legitimate SQL.

I’m not going to cover the console too much more for right now. The above should have given you a flavor of what you can do, however. One final thing I’ll mention around this topic: if you do play around with the database from the console, you might end up with lots of data that you don’t want. That’s fine because, remember, you can always run this:

rake db:seed

That will delete everything in the database and populate it again with the Mars object.

Testing the Application … Again

I’ve actually started to significantly add to the Rails generated logic, so I’ll run my tests again:

rake test

Now I get two failures. Specifically both of these test methods in planets_controller_test.rb:

PlanetsControllerTest#test_should_create_planet
PlanetsControllerTest#test_should_update_planet

It doesn’t take a huge leap to realize that since I added validations, the default tests that Rails provided for creating and updating a planet are now failing. Why? Let’s take a look at planets_controller_test.rb and those methods in particular:

Here a planet instance (@planet) is being created and updated. But where is that instance coming from? Well, there is also the following method in this test:

Okay, so there’s our @planet instance, but what is “:one” referring to? This is referring to an item in the test/fixtures/planets.yml file. In short, :one refers to a test fixture.

You probably remember from the first post that Rails creates different databases for different environments: development, testing, and production. The key thing there is that Rails creates an environment explicitly for testing. The test fixtures are only used in that environment. Any data in the development environment is not used. So my Mars data point (from seeds.rb) is not visible to the tests. The fixture data that you end up with my default is:

The fixture data is meant to represent data for the model. Clearly the generator that created the fixture had no idea what relevant data was but it did know the type of data and thus populated some attributes with “dummy values” to at least allow the fixture data to work. Yet, given the fixture data that is there, you might wonder: why are these tests failing then? The fixture is clearly using data for all of the required values. Let’s consider the first error more closely:

PlanetsControllerTest#test_should_create_planet
"Planet.count" didn't change by 1.
Expected: 3
  Actual: 2

What this is telling me is that when the “should_create_planet” test was run, the count of planet records in the database didn’t go up. Three planets were expected, but only two were in there. Wait. Three? Ah — now I get it. The fixture data — :one and :two — are loaded automatically by the test commands. Yet, where would you have found this out? Well, if you look at the test files that Rails generated, they all refer to a test_helper.rb file. If you check that file, you’ll see this:

That fixtures() method loads the fixture data corresponding to a given model or, in this case, fixture for all models. If, for some reason, I only wanted my planets fixtures being loaded, I could change that line to this:

So now I think I see what’s happening. Remember the uniqueness validation I put in? Since the controller test is always using the test fixture called :one, the create test will reuse a data point that has the same name in multiple tests. Put another way, the two fixture items were already in the database. Then the create test happened, essentially using one of those same fixtures. Thus the create test was using data that had a name attribute that matched an existing entry in the database. So the failure makes sense. This is actually proving that my validation works. But I still have a failing test. So what do I do about it?

Adding Fixtures

I mentioned that my data from seeds.rb is not available in the test environment. (And, conversely, the text fixture data is not available in the development environment.) So I think what I’d like to do first is provide some actual data as a text fixture; I may as well just use my Mars data example. So I’ll add this to planets.yml:

Let’s stop for a moment and consider fixtures. Fixtures are essentially textual representations of table data written in YAML format. Fixtures are used by your tests to populate your database with data to test against. In Rails, any test fixture is a specification of the initial contents of the model (or models) under test. So just keep that in mind: fixtures contain data that represents a specific record of a specific model. Each fixture file contains the data for a single model. In my case, I only have one model — Planet — and so there is only one fixture file — planets.yml. Any data in there should correspond to the schema for a planet. If I created more models, I would have a corresponding fixture file for each. Each entry in a given fixture file is given a name (one, two, mars) and that name represents a row in the database.

Testing the Controller

As we’ll see, the model is easily tested with unit tests since the model could be tested in isolation. The controller, however, requires a bit more integration with a context. Specifically, I need Rails to set up request and response objects for me so that those objects act just like the live requests and responses that would occur when a user is interaction with the application via a browser. If you look at the test/controllers/planets_controller_test.rb file, you’ll see that each test is generated by rails to execute a specific request for a specific action on the controller.

An important convention of Rails is that what it calls a “functional test” will define methods that correspond to HTTP verbs. You use these methods to send requests to the server. So, before using my fixture, let’s look at the generated index method:

Here you can see the first line of the test makes a GET request (using the get() method) for the index action using get the :index symbol. The second line then asserts that the response received was “:success”. This corresponds to a HTTP response code of 200, so I could have also done this: assert_response 200.

The final line in the test checks that the correct instance variables were assigned. In the setup() method, I set an instance variable called @planets that contains the planets collection. So this final assertion checks that @planets was, in fact, assigned and thus is not nil. Keep in mind that the setup() method is executed before every test case. In this case, the setup method assigns the :mars record from the fixtures to the instance variable @planet.

There is one other test that I’ve found you can do and I’m somewhat surprised that Rails doesn’t put this in the above test automatically. You can check that the proper template was rendered in response to the request. I’ll add this to the test:

I’m expected to see the index template (from app/views/planets/index.html.erb) to be rendered and that’s what I test for.

I can do the same for the “should show planet” test, also adding in the check for the correctly assigned instance variable:

Here I treat the results of assigns(:planet) as any Planet object. This means I can call a method on it — in this case, the valid? method. I’m doing this here so that I not only assert that there is an instance variable named @planet, but also assert that it contains a valid Planet object.

With my test data fixture in place, I’ll change the setup() method in planets_controller_test.rb to reference it:

This really won’t do anything to fix the “should create planet” test (although, oddly, it does seem to fix the “should update planet” test). The reason for this is that I really haven’t changed the underlying problem. What I really need is “on the fly” data. So — what about if I could leverage my fixture but just change the name before the data is saved into the database? A simple way to do that would be to change the test like this:

Notice how I just hard coded a name rather than use the existing one. Now all tests pass. Yet, I have to say, this feels very sloppy to me.

To test the create action, I need to submit some form parameters to create a valid planet. I just need to pass a hash of parameters that contains a valid set of planet attributes. This is just what happens when I use the HTML form, since Rails converts HTML form parameters into a hash object. Notice that this test uses a POST request since an HTTP POST method is required to put the data into the database from the web form. Here’s a way I found to make the test look a little nicer:

Here I leverage my fixture and pass in the fixture using the Active Record attributes() method call. Something to notice in this test is that Rails has the request generate a post to create a planet inside the assert_difference method block. The assert_difference method basically just takes a parameter and compares it with itself after running the block content, expecting the difference to be 1 by default. So in this case, it expects Planet.count to return the same count plus 1 after running the post request to create an planet.

I changed the update planet test in a similar way:

At this point all of my tests work, and I feel like I better understand what they are doing and why they work.

Keep in mind that the testing I was doing here was solely on the controller. I realized that the test fixture I added was really a convenience for the model, as opposed to the controller. Meaning, what I really should be focusing on are some model tests. This was hit home even more when I realized that with my latest changes to the controller tests, I actually was no longer testing the unique constraint data integrity check.

The planet_test.rb is the file that Rails created to hold the unit tests for the model. One thing that needs to be tested is the ability to make sure two records cannot have the same name. This is basically the same problem with the fixture data that I wrestled with in the controller test, but there I was trying to avoid the problem; here I’m trying to hit it head on. Another thing that needs to be tested is making sure that a data record without the valid attributes present will not be saved. Which is interesting, of course, because all of my fixture data has all the relevant data.

Level of Testing

Before going forward, let’s consider a few realities of testing in Rails. Any tests I write for the model, which I’m going to be doing shortly, are essentially unit tests. I’m taking a model in isolation and manipulating it to see what happens. However, the tests for the controllers are a bit different. There are, by necessity, what Rails terms “functional” tests but are really a type of “integration test”. I can test a model entirely outside the context of a functioning web application; I don’t need requests or responses nor do I have to worry about the URL a user starts from or ends at. That’s not the case with controllers where all of those things matter.

So let’s add a test that actually does check if a unique name is enforced. I added this to test/models/planet_test.rb:

Notice line 4 there? Rails has a convention that for each fixture it loads into a test, a method will be defined that has the same name as the fixture. Using that you can do what I did in this test: call the method to access preloaded model objects that contain the fixture data. In this case, I just pass the method the name of the row as defined in the YAML fixture file, in this case “mars”. That returns a model object that contains the data for that row.

This test is based on the fact that the database already includes an entry for Mars, which I know because Mars is part of my fixture data and I know all my fixture data gets preloaded. The test gets the name of that existing row and then attempts to create a new planet using that same name, which allows me to test the uniqueness constraint. Now I’ll add another test that makes sure that the required fields for the planet must be in place:

Here I simply create a new Planet model instance that has no attributes set. I check that the object is invalid and then I check if the specific attributes that are required have errors associated with them and if those errors are the ones I expect. You’ll note that I assert that planet.save returns false and this is an important part of the test that I’ve found some Rails developers leave off. Even though I got errors, I still want to make certain that the data item can’t be saved to the database.

Let’s add another test to make sure that planets can be found when they are in the database:

This is basically testing that a planet of the given id can be found. I do this by grabbing the id attribute from the :mars fixture. I tend use the find() method on the Planet model to retrieve that particular record with that id. I use the assertion assert_nothing_raised because the find() method raises an exception if the record can’t be found in the database. So if no exception is raised, that would mean the record was found.

As a final aspect of testing my model, I want to check the valid create, update, and delete/destroy actions. I realize the controller was handling some of this from the standpoint of a full request but, to me, the most responsible time to be catching an issue is in the model itself, since the model is responsible for the validations. If the model isn’t working, I want to know that long before anything gets to the controller. So here’s a create test:

Here I create a data item based on my fixture but then I change the name of the data item so I don’t run into the uniqueness constraint validation. Then I simply make sure the data can be saved by asserting that the save operation took place. Here’s an update test:

Fairly simple; I get my planet fixture and then I assert that changing the name of the planet, via update_attributes, works.

Finally, here’s a delete/destroy test:

Here the assert_raise assertion let’s me specify the class of the exception that I expect to be raised for the action of finding the specific planet. So here, because I’ve deleted the planet, I expect Active Record to respond with a RecordNotFound exception when I try to find the planet that I just deleted.

Wrapping Up

At this point, I feel really good about my model and controller tests. I feel like I have the solid basis for a working application. I also feel like I have an understanding of how Rails actually works. This is probably a good place to stop this series since the goal was to start development of a Rails application. I’m debating whether to create another series of posts on “Creating a Rails application” where I’ll actually take something through to completion. I’m not entirely sure such posts would benefit anyone other than me. (Then again, I suppose the same could be said for the two posts in this series!)

In any event, Rails has convinced me that it is a viable platform for me to work with, once I got comfortable with it and reduced some of that “behind the scenes magic” to tangibles that I could manipulate and thus control.

Share

This article was written by Jeff Nyman

Anything I put here is an approximation of the truth. You're getting a particular view of myself ... and it's the view I'm choosing to present to you. If you've never met me before in person, please realize I'm not the same in person as I am in writing. That's because I can only put part of myself down into words. If you have met me before in person then I'd ask you to consider that the view you've formed that way and the view you come to by reading what I say here may, in fact, both be true. I'd advise that you not automatically discard either viewpoint when they conflict or accept either as truth when they agree.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.