• No results found

Testing database applications can be a serious pain. It’s made worse

when database access code is sprinkled throughout the application. You can never seem to test the smallest chunk of code without first firing up the database and then spoon-feeding it enough data to make the code do something interesting. We programmers have a marketing term for that—

bad coupling.

Report erratum

TESTINGMODELS 134

Rails promotes good testing (and good design) by enforcing a structure for your application whereby you create models, views, and controllers as separate chunks of functionality. All the application’s state and business rules that apply to its state are encapsulated in models. And Rails makes it easy to test models in isolation, so let’s get right to it.

Your First Test, Already Waiting

The first model we created for the Depot application, way back in Chap- ter6,Task A: Product Maintenance, on page49, wasProduct. Let’s see what

kind of test goodies Rails generated inside the file test/unit/product_test.rb

when we created that model.

require ... page480

require File.dirname(__FILE__) + '/../test_helper' class ProductTest < Test::Unit::TestCase

fixtures :products

def setup

@product = Product.find(1)

end

# Replace this with your real tests.

def test_truth

assert_kind_of Product, @product

end end

OK, our second decision—how to write tests—has already been made for

us. The fact that theProductTestis a subclass of theTest::Unit::TestCaseclass

tells us that Rails generates tests based on the Test::Unit framework that comes preinstalled with Ruby. This is good news because it means if we’ve already been testing our Ruby programs with Test::Unit tests (and why wouldn’t you want to?), then we can build on that knowledge to test Rails applications. If you’re new to Test::Unit, don’t worry. We’ll take it slow. Now, what’s with the generated code inside of the test case? Well, when theProductmodel was created, Rails thought it would be a good idea if we

actually tested that a Product object could be fetched from the database.

So, Rails generated a setup( ) method that goes out and attempts to load

the product with a primary key value of 1 from the database. Then it tucks

the product away in the@productinstance variable for later use.

Next we see a test method calledtest_truth( ), with a comment above it hint-

ing that we have work to do. But before we break a sweat, let’s just see if the test passes.

depot> ruby test/unit/product_test.rb

Loaded suite test/unit/product_test Started

E

Finished in 0.091436 seconds.

Report erratum

TESTINGMODELS 135

1) Error:

test_truth(ProductTest):

ActiveRecord::StatementInvalid: Table 'depot_test.products'

doesn't exist: DELETE FROM products . . .

1 tests, 0 assertions, 0 failures, 1 errors

Guess it wasn’t the truth, after all. The test didn’t just fail, it exploded!

Thankfully, it leaves us a clue—it couldn’t find theproductsdatabase table.

But we know there is one because we used it when we manually tested the Depot application using a web browser. Hmph.

A Database Just for Tests

Remember back on page50when we created three databases for the Depot

application? It seemed like overkill at the time. One was for development use—that’s the one we’ve been using so far. One was for production use— we hope that happens someday soon. And one was for (drum roll, please) testing. Rails unit tests automatically use the test database, and there are no products in it yet. In fact, there are no tables in it yet.

So, as a first step, let’s load our schema into the test database. It turns out there are two ways of doing this, assuming the test database has already been created. If you’ve been following along and building a schema defi-

nition in the file db/create.sql, you can simply use it to populate the test

database.

depot> mysql depot_test < db/create.sql

If, however, you’ve been building the schema in the development database

by hand, then you might not have a valid create.sql script. No worries—

Rails has a handy mechanism for cloning the structure (without the data)

from the development database into the test database.1 Simply issue the

following command in your application’s top-level directory. (We’ll have

more to say about all this rake business in Section 12.6, Running Tests

with Rake, on page165.)

depot> rake clone_structure_to_test

OK, so we now have a schema in our test database, but we still don’t have any products. We could enter data by hand, perhaps by typing some SQL insertcommands, but that’s tedious and somewhat error prone. And if we later write tests that modify the data in the database, we’ll somehow have to get our initial data reloaded before we can run tests again.

Rails has the answer—test fixtures.

1Currently supported only when using MySQL, PostgreSQL, or SQLite.

Report erratum

TESTINGMODELS 136

Test Fixtures

The word fixture means different things to different people. In the world

of Rails, a test fixture is simply a specification of the initial contents of a

model. So, if we want to ensure that ourproducts table starts off with the

correct contents at the beginning of every unit test, we need only specify those contents in a fixture and Rails will take care of the rest.

You specify the fixture data in files in thetest/fixturesdirectory. These files

contain test data in either Comma-Separated Value (CSV) format or YAML format. For our tests we’ll use YAML, as it’s preferred. Each YAML fixture file contains the data for a single model. The name of the fixture file is significant; the base name of the file must match the name of a database

table. As we need some data for a Product model, which is stored in the

products table, we create a file calledproducts.yml. (Rails created an empty version of this fixture file when it generated the corresponding unit test.)

File 115 version_control_book:

id: 1

title: Pragmatic Version Control description: How to use version control image_url: http://.../sk_svn_small.jpg

price: 29.95

date_available: 2005-01-26 00:00:00 automation_book:

id: 2

title: Pragmatic Project Automation description: How to automate your project image_url: http://.../sk_auto_small.jpg

price: 29.95

date_available: 2004-07-01 00:00:00

The format of the fixture file is straightforward. It contains two fixtures

named version_control_book and automation_book, respectively. Following

the name of each fixture is a set of key/value pairs representing the col- umn names and the corresponding values. Note that each key/value pair must be separated by a colon and indented with spaces (tabs won’t do). Now that we have a fixture file, we want Rails to load up the test data

into the products table when we run the unit test. Not surprisingly, Rails

assumed this, so the fixture loading mechanism is already in place thanks

to the following line in theProductTest.

File 124 fixtures :products

Thefixtures( ) method automatically loads the fixture corresponding to the given model name at the start of each test method in this test case. By convention, the symbol name of the model is used, which means that

using:productswill cause theproducts.ymlfixture file to be used.

Report erratum

TESTINGMODELS 137

David Says. . .

Picking Good Fixture Names

Just like the names of variables in general, you want to keep the names of fixtures as self-explanatory as possible. This increases the readability of the tests when you’re asserting that@valid_order_for_fred is indeed Fred’s valid order. It also makes it a lot easier to remember which fixture you’re supposed to test against without having to look upp1ororder4. The more fixtures you get, the more important it is to pick good fixture names. So, starting early keeps you happy later.

But what to do with fixtures that can’t easily get a self-explanatory name like@valid_order_for_fred? Pick natural names that you have an easier time associating to a role. For example, instead of using order1, use christ- mas_order. Instead of customer1, use fred. Once you get into the habit of natural names, you’ll soon be weaving a nice little story about howfred is paying for hischristmas_orderwith hisinvalid_credit_cardfirst, then paying hisvalid_credit_card, and finally choosing to ship it all off toaunt_mary. Association-based stories are key to remembering large worlds of fixtures with ease.

Fixtures for Many-to-Many Associations

If your application has models with many-to-many associations that you’d like to test, then you’ll need to create a fixture data file that represents

the join table. Say, for example, a Category and a Product declare their

association with each other using the has_and_belongs_to_many( ) method

(described starting on page 230). By convention the join table will be

namedcategories_products. Thecategories_products.ymlfixture data file that

follows includes example fixtures for the associations.

File 112 version_control_categorized_as_programming: product_id: 1 category_id: 1 version_control_categorized_as_history: product_id: 1 category_id: 2 automation_categorized_as_programming: product_id: 2 category_id: 1 automation_categorized_as_leisure: product_id: 2 category_id: 3 Report erratum

TESTINGMODELS 138

Then you’d just need to provide all three fixtures to thefixtures( ) method in

the test case.

fixtures :categories, :products, :categories_products

Create and Read

Before running theProductTesttest case, let’s beef up the test a tad. First

things first: we rename test_truth( ) to test_create( ) to better explain what

we’re testing. Then, intest_create( ), we check that the@product found in

setup( ) matches the corresponding fixture data.

File 124 def test_create

assert_kind_of Product, @product assert_equal 1, @product.id

assert_equal "Pragmatic Version Control", @product.title

assert_equal "How to use version control", @product.description assert_equal "http://.../sk_svn_small.jpg", @product.image_url assert_equal 29.95, @product.price

assert_equal "2005-01-26 00:00:00",

@product.date_available_before_type_cast

end

Think of Test::Unit assertions as the computer’s way of remembering what result you expect from code so you don’t have to. The assertions all follow basically the same pattern. The first parameter is the result you expect, and the second parameter is the actual result. If the expected and the actual don’t match, then the test fails with a message indicating what went

wrong. The first assertion in the preceding code simple meansexpect the

product id to be 1, and complain if it isn’t. The only slightly funky thing is

the last assertion. It uses the_before_type_cast suffix to get the raw value

of date_available. Without this suffix, we might be comparing against a

Time object. (We talk more about this on page194.)

So, now that we have everything hooked up—a test and data to run it against—let’s give it a jolly good ol’ tug.

depot> ruby test/unit/product_test.rb

Loaded suite test/unit/product_test Started

.

Finished in 0.0136043 seconds.

1 tests, 7 assertions, 0 failures, 0 errors

Don’t you just love it when a plan comes together? This may not seem like much, but it actually tells us a lot: the test database is properly config-

ured, the products table is populated with data from the fixture, Active

Record was successful in fetching a given Product object from the test

database, and we have a passing test that actually tests something. Not too shabby.

Report erratum

TESTINGMODELS 139

Update

OK, so the previous test verified that the fixture created a Product that

could be read from the database. Now let’s write a test that updates a Product.

File 124 def test_update

assert_equal 29.95, @product.price @product.price = 99.99

assert @product.save, @product.errors.full_messages.join("; ") @product.reload

assert_equal 99.99, @product.price

end

Thetest_update( ) method first makes sure that the price of the product rep-

resented in the@productinstance variable matches the price listed in the

products.ymlfile. The product’s price is changed, and the updated product is saved back to the database. Then the test reloads the attributes of the Productfrom the database and asserts that the reloaded @product reflects the changed price.

Here’s the catch: we don’t necessarily know the order in which the test methods run. If the update test runs before the create test, we might

think we’d have a problem, because test_update( ) alters the price in the

database andtest_create( ) method runs it still expects the product’s price

to be the original value in the products.yml fixture file. Let’s roll the dice

and see what happens.

depot> ruby test/unit/product_test.rb

Loaded suite test/unit/product_test Started

..

Finished in 0.0274435 seconds.

2 tests, 10 assertions, 0 failures, 0 errors

Lo and behold, it worked! It turns out that each test method is iso-

lated from changes made by the other test methods in the same test case

because of a carefully orchestrated sequence of actions. Thetest_update( )

method cannot clobber thetest_create( ) one.

Test Data Life Cycle

We’ve seen the fixtures( ) andsetup( ) methods in action. They both prepare

data for the test methods to use, but they do their work at different points

of the test data life cycle. When theProductTestis run, for example, three

things are guaranteed to happen beforeeverytest method.

1. Every row in theproducts table of the test database is deleted. This

is OK, because depot_test is a test database. It’s considered to be

Report erratum

TESTINGMODELS 140

transient, so it’s perfectly acceptable for Rails to empty it out to give each of our tests a fresh place to start each time they run.

2. All the test data listed in the products.yml fixture file (products ver-

sion_control_book andautomation_book, in this case) is loaded into the products table of the test database.

3. After all the test fixtures have been loaded (we could have listed more

than one), the setup( ) method is run. In this case, an Active Record

finder method is used to fetch the Product object corresponding to

a primary key value of 1. The returned object is assigned to the

@productinstance variable.

Here’s the bottom line: even if a test method updates the test database, the database is put back to its default state before the next test method is run. This is important because we don’t want tests to become dependent on the results of previous tests.

Destroy

Destroying aProductmodel object deletes the corresponding row from the

database. Attempting to find theProduct causes Active Record to throw a

RecordNotFoundexception. We can test that, too.

File 124 def test_destroy

@product.destroy

assert_raise(ActiveRecord::RecordNotFound) { Product.find(@product.id) }

end

Validation

TheProductmodel validates, among other things, that the price is greater than zero. Only then will Active Record save the product away in the database. But how are we going to test this bit of validation? No problem.

If the price is less than or equal to zero, then theProductisn’t saved in the

databaseand a message is added to theerrorslist.

File 124 def test_validate

assert_equal 29.95, @product.price @product.price = 0.00

assert [email protected]

assert_equal 1, @product.errors.count

assert_equal "should be positive", @product.errors.on(:price)

end

That works. Unfortunately, adding more test methods has introduced a different problem—the tests are brittle. If we change the test data in the fixture file, the tests will break. Rails to the rescue.

Report erratum

TESTINGMODELS 141

Keeping Tests Flexible

Duplicating information in the tests that’s already specified in the fixture file makes for brittle tests. Thankfully, Rails makes it easy to keep test data in one place—the fixture.

When a fixture is loaded, it’s put into a Hash object referenced by an

instance variable of the test case. For example, the:productsfixture is con-

veniently loaded into the@productsinstance variable. That way, instead of

hard-coding the expected values in our tests, we can access the test data using hash semantics.

File 124 def test_read_with_hash

assert_kind_of Product, @product

vc_book = @products["version_control_book"] assert_equal vc_book["id"], @product.id assert_equal vc_book["title"], @product.title

assert_equal vc_book["description"], @product.description assert_equal vc_book["image_url"], @product.image_url assert_equal vc_book["price"], @product.price

assert_equal vc_book["date_available"], @product.date_available_before_type_cast

end

This test verifies that the data in theversion_control_bookfixture in theprod-

ucts.yml fixture file matches the equivalent product in the test database. That is, after all, what the fixture is supposed to do.

But it gets better. Each named fixture is also automatically “found” using an Active Record finder method and put in an instance variable named for

the fixture. For example, because theproducts.ymlfixture file contains two

named fixtures (version_control_book and automation_book), the test meth-

ods ofProductTestcan use the instance variables@version_control_book and

@automation_book, respectively. The correlation between a fixture in a YAML fixture file, the table in the test database, and the instance vari-

ables in the test case are shown in Figure12.1, on the following page.

This means we can rewrite the test above to use the@version_control_book

product loaded by the fixture.

File 124 def test_read_with_fixture_variable

assert_kind_of Product, @product

assert_equal @version_control_book.id, @product.id assert_equal @version_control_book.title, @product.title

assert_equal @version_control_book.description, @product.description assert_equal @version_control_book.image_url, @product.image_url assert_equal @version_control_book.price, @product.price

assert_equal @version_control_book.date_available, @product.date_available

end

Report erratum

TESTINGMODELS 142 products id 1 2 title

Pragmatic Version Control Pragmatic Project Automation

description How to ... How to ... image_url http://.../sk_svn_small.jpg http://.../sk_auto_small.jpg date_available 2005-02-26 2004-07-01 price 29.95 29.95 (test database) 1 version_control_book: id: 1

title: Pragmatic Version Control description: How to use version control image_url: http://.../sk_svn_small.jpg price: 29.95 date_available: 2005-02-26 automation_book: id: 2

title: Pragmatic Project Automation description: How to automate your project image_url: http://.../sk_auto_small.jpg price: 29.95 date_available: 2004-07-01 products.yml 1 inserted into

class ProductTest < Test::Unit::TestCase fixtures :products @products["version_control_book"] and @version_control_book end assigned to loaded by

Figure 12.1: Test Fixtures

This test simply demonstrates that the fixture named version_control_book

was automatically loaded into the @version_control_bookinstance variable.

This raises a question: if fixtures are automatically loaded into instance

variables, then what’s thesetup( ) method good for? It turns out thatsetup( )

is still quite useful in several cases that we’ll see a bit later.