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:
1 2 3 |
class Planet < ActiveRecord::Base validates :name, :diameter, :mass, presence: true end |
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:
1 2 3 |
class Planet < ActiveRecord::Base validates_presence_of :name, :diameter, :mass end |
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:
1 2 3 4 |
class Planet < ActiveRecord::Base validates_presence_of :name, :diameter, :mass validates :name, uniqueness: true end |
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:
1 2 3 4 |
class Planet < ActiveRecord::Base validates_presence_of :name, :diameter, :mass validates_uniqueness_of :name 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
test "should create planet" do assert_difference('Planet.count') do post :create, planet: { details: @planet.details, diameter: @planet.diameter, facts: @planet.facts, image_url: @planet.image_url, mass: @planet.mass, name: @planet.name } end assert_redirected_to planet_path(assigns(:planet)) end test "should update planet" do patch :update, id: @planet, planet: { details: @planet.details, diameter: @planet.diameter, facts: @planet.facts, image_url: @planet.image_url, mass: @planet.mass, name: @planet.name } assert_redirected_to planet_path(assigns(:planet)) end |
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:
1 2 3 |
setup do @planet = planets(:one) end |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
one: name: MyString image_url: MyString diameter: 1.5 mass: 1.5 details: MyText facts: MyText two: name: MyString image_url: MyString diameter: 1.5 mass: 1.5 details: MyText facts: MyText |
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:
1 2 3 4 5 6 7 |
class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all end |
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:
1 |
fixtures :planets |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
mars: name: Mars image_url: mars.jpg diameter: 6794 mass: 6.4219e23 details: Mars is the only planet whose surface can be seen in detail from the Earth. It is reddish in color, and was named after the bloody red God of war of the ancient Romans. Mars is the fourth closest planet to the sun. The diameter of Mars is 4,200 miles, a little over half that of the Earth. Mercury is the only planet smaller than Mars. facts: Mars orbits the sun every 687 Earth days. The Martian day is about half a hour longer than Earth. At its brightest, Mars outshines every other planet apart from Venus. The thin atmosphere of Mars is made of mostly carbon dioxide. |
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:
1 2 3 4 5 |
test "should get index" do get :index assert_response :success assert_not_nil assigns(:planets) end |
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:
1 2 3 4 5 6 |
test "should get index" do get :index assert_response :success assert_template 'index' assert_not_nil assigns(:planets) end |
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:
1 2 3 4 5 6 7 |
test "should show planet" do get :show, id: @planet assert_response :success assert_template 'show' assert_not_nil assigns(:planet) assert assigns(:planet).valid? end |
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:
1 2 3 |
setup do @planet = planets(:mars) end |
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:
1 2 3 4 5 6 7 8 9 10 |
test "should create planet" do assert_difference('Planet.count') do post :create, planet: { details: @planet.details, diameter: @planet.diameter, facts: @planet.facts, image_url: @planet.image_url, mass: @planet.mass, name: 'Jupiter' } end assert_redirected_to planet_path(assigns(:planet)) end |
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:
1 2 3 4 5 6 7 8 |
test "should create planet" do assert_difference('Planet.count') do @planet.name = 'Jupiter' post :create, planet: @planet.attributes end assert_redirected_to planet_path(assigns(:planet)) end |
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:
1 2 3 4 |
test "should update planet" do patch :update, id: @planet, planet: @planet.attributes assert_redirected_to planet_path(assigns(:planet)) end |
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:
1 2 3 4 5 6 7 8 9 10 11 |
class PlanetTest < ActiveSupport::TestCase test 'planet must have a unique name' do planet = Planet.new( name: planets(:mars).name, diameter: 1, mass: 1) assert planet.invalid? assert_equal ["has already been taken"], planet.errors[:name] end end |
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:
1 2 3 4 5 6 7 8 9 10 11 |
test 'planets must have values for required attributes' do planet = Planet.new assert planet.invalid? assert planet.errors[:name].any? assert planet.errors[:diameter].any? assert planet.errors[:mass].any? assert_equal ["can't be blank"], planet.errors[:name] assert_equal ["can't be blank"], planet.errors[:diameter] assert_equal ["can't be blank"], planet.errors[:mass] assert !planet.save end |
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:
1 2 3 4 |
test 'planets can be found in the database' do planet_id = planets(:mars).id assert_nothing_raised { Planet.find(planet_id) } end |
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:
1 2 3 4 5 |
test 'planets can be created' do planet = planets(:mars) planet.name = 'Jupiter' assert planet.save end |
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:
1 2 3 4 |
test 'planets can be updated' do planet = planets(:mars) assert planet.update_attributes(name: 'Jupiter') end |
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:
1 2 3 4 5 |
test 'planets can be deleted' do planet = planets(:mars) planet.destroy assert_raise(ActiveRecord::RecordNotFound) { Planet.find(planet.id) } end |
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.