In Part 1 of the DB2 and Ruby on Rails series, we introduced the IBM_DB Ruby driver, Rails migration, and a Team Room application. In Part 2, you learned how to build on the existing Team Room application to take advantage of DB2® pureXML™ support in a Rails application.
Now that the Team Room application has grown and is becoming more complex, it is time to ensure that the application is doing what is expected. Naturally, the next step is to test our Team Room application. This third article shows you how testing works in a DB2 on Rails environment, and how easy it is to write tests within the Ruby on Rails framework.
The Rails framework provides a convenient means of testing web applications thanks to its built-in support for testing. In Rails, you can write tests after you have a core application up and running, or as you write your application, and even before you start writing the application [referred to as Test Driven Development (TDD)].
Testing the Team Room application
When you create a new Rails project, Rails generates the test infrastructure for you. If you go into our Rails Team Room project directory in D:\rails\teamroom\, you will notice the \test subdirectory. Under \test, you will see five existing directories and a helper file:
Listing 1. Test directory contents
/unit /functional /fixtures /integration /mocks test_helper.rb |
You put all your tests into the /test directory, and place specific tests into each of the five subdirectories according to nature and function of each. Each of the components within the /test directory is explained below:
In Rails, tests written to test models are called unit tests. Generally, you write one unit test per model. In a unit test, you test anything that could possibly break your model. Basic tests should include the testing of validation code and assertions, and database operations such as Create, Read, Update and Delete (CRUD).
Tests written to test controllers are called functional tests. They test the application at a higher level than unit tests. Again, generally you write one functional test for each controller you have. Examples of functional tests include testing for a successful Web request, redirection to the correct page, proper authentication, and the correct responses for specific actions.
A fixture specifies the contents within a model. You specify data in a fixture, and tell Rails to load the data when you run the unit tests. This is somewhat similar to the import or load function in DB2.
A mock is a "fake" object that allows you to focus on testing important features within the scope of the Rails project, rather than on interactions with external systems that you have no control over. For example, our Team Room application notifies our users by email when new documents are added to specific subjects to which they've subscribed. We do not have an email validation system to verify that the email supplied by a user is in fact a valid email address. Rather, we rely on an error report from the email account provider or the domain server to validate the email addresses provided by our users. In order to test our Team Room notification function without accessing the external network or having an email validation system in place, we can write mock test methods.
An integration test is a test that spans multiple controllers and actions. As the name suggests, this test ensures that different controllers and actions work together as expected. Integration tests are generally more complete as they cover high-level interactions between various components within the Rails application.
The test_helper.rb file establishes a number of default Rails behaviors common to multiple test cases. For example, in test_helper.rb, notice that the RAILS_ENV environmental variable is set to "test" so that Rails uses the test database (configured in the database.yml file) for testing. All unit tests load the test_helper.rb file in Rails. Custom assertions can also be defined in test_helper.rb so that multiple tests within the same Rails application can share and use these custom assertions.
Setting up various database environments
In Part 1 of the DB2 and Ruby on Rails series, we edited the D:\rails\teamroom\config\database.yml file in order to make a connection to the DB2 XMLDB development database. Below the "development" section in the YAML file, there are two additional sections named "test" and "production" that we did not explicitly fill out. As the name suggests, you specify your test database environment in the "test" section, and your production database environment in the "production" section. In real life, your development or test environment may point to the same database, but your production environment would certainly be separated from the development or test database. Now, let's create our TESTDB test database and set up our test environment in the database.yml file.
Create the TESTDB database:
db2 create db testdb using codeset utf-8 territory us
Edit the database.yml to look like the following:
Listing 2. The database.yml file
# IBM DB2 Database configuration file # # Install the IBM DB2 driver and get assistance from: # http://www.alphaworks.ibm.com/tech/db2onrails development: adapter: ibm_db database: xmldb username: user password: secret schema: teamroom application: TeamRoom account: devuser workstation: devbox # == remote TCP/IP connection (required when no local database catalog entry available) # host: bigserver // fully qualified hostname or IP address # port: 50000 // data server TCP/IP port number test: adapter: ibm_db database: testdb username: testuser password: secret schema: teamroom application: TeamRoom account: testuser workstation: testbox production: adapter: ibm_db database: proddb username: produser password: secret schema: teamroom application: TeamRoom account: produser workstation: prodbox |
For testing purposes, we want to ensure the test database has the same table structures as the development database. Instead of having to manage DDL scripts to load the schema, we can use Rake commands to help with creating the test environment. Running rake --tasks command shows a few of the relevant commands for building or emptying out the test database:
Listing 3. Rake --tasks output
rake db:test:clone # Recreate the test database from the current
# environment's database schema
rake db:test:clone_structure # Recreate the test databases from the development
# structure
rake db:test:prepare # Prepare the test database and load the schema
rake db:test:purge # Empty the test database
|
We will build the tables in our database using the rake db:test:prepare command. Running this command will duplicate the schema from the development database into the test database:
Listing 4. rake db:test:prepare
D:\rails\teamroom> rake db:test:prepare (in D:\rails\teamroom) |
We can now connect to the testdb database and verify that all the tables were successfully created:
Listing 5. Tables in testdb
D:\rails\teamroom>db2 list tables for schema teamroom Table/View Schema Type Creation time ------------------------------- --------------- ----- -------------------------- DOCUMENTS TEAMROOM T 2007-06-05-11.21.46.343002 SCHEMA_INFO TEAMROOM T 2007-06-05-11.21.48.306001 SUBJECTS TEAMROOM T 2007-06-05-11.21.46.633003 SUBJECTS_SUBSCRIPTIONS TEAMROOM T 2007-06-05-11.21.47.004000 SUBSCRIPTIONS TEAMROOM T 2007-06-05-11.21.47.194005 USERS TEAMROOM T 2007-06-05-11.21.47.595003 XML_CONTENTS TEAMROOM T 2007-06-05-11.21.47.955001 7 record(s) selected. |
With all the tables defined in the test database, you can now load test data. Although you can do this manually through SQL inserts or imports, the less tedious (and more manageable) approach would be to make use of text fixtures, which we will talk about shortly.
One of the key libraries shipped with Ruby (1.8.1 and higher) is test/unit. To write Ruby test scripts, we'll:
- Specify
"require 'test/unit'"at the top of the .rb file. This statement will load the Test::Unit module. - After the require statement, a class declaration stating that this class is a subclass of Test::Unit::TestCase must be added (as shown in below in Listing 6).
Listing 6. Specifying "require 'test/unit'" for the unit test
require 'test/unit' class SubTestCase < Test::Unit::TestCase ... end |
In the Team Room application path's test\unit subdirectory, you will find .rb files which correspond to all model objects created through scaffolding. The files will have a "_test" appended to the object name.
When we are creating test scripts under the test\unit subdirectory, the require 'test/unit'statement does not need to be specified.
The test_helper.rb file, which is referenced in the existing require statement, already contains the necessary information.
Listing 7. The test\unit path listing
D:\rails\teamroom\test\unit>dir
Volume in drive D has no label.
Volume Serial Number is C8D3-5819
Directory of D:\rails\teamroom\test\unit
06/05/2007 09:27 AM <DIR> .
06/05/2007 09:27 AM <DIR> ..
06/05/2007 09:27 AM 208 customer_info_test.rb
05/31/2007 07:42 PM 199 document_test.rb
06/02/2007 05:14 PM 251 subjects_subscription_test.rb
05/31/2007 07:42 PM 315 subject_test.rb
05/31/2007 07:42 PM 1,211 subscription_mailer_test.rb
05/31/2007 07:42 PM 207 subscription_test.rb
05/31/2007 07:42 PM 191 user_test.rb
05/31/2007 07:42 PM 204 xml_content_test.rb
8 File(s) 2,786 bytes
2 Dir(s) 15,359,959,040 bytes free
|
Methods with the "test_" prefix
All test method names in Ruby must start with "test" as the first four characters, or the testing framework won't recognize it as a test. Without the "test" prefix, it will be treated as a normal Ruby method and the testing framework won't run it automatically.
Listing 8. A trivial unit test
require File.dirname(__FILE__) + '/../test_helper'
class Sub_Test < Test::Unit::TestCase
def test_truth
assert true
end
end
|
We can now save the file as sub_test.rb. When executed, it will result in the following output.
Listing 9. Running sub_test.rb
D:\rails\teamroom\test\unit>ruby sub_test.rb Loaded suite sub_test Started . Finished in 0.631 seconds. 1 tests, 1 assertions, 0 failures, 0 errors |
The above simple test scenario performed one assert and resulted in a successful scenario due to the "assert true" statement. Assertions are the tests and as you will see in the following section, there are a number of assert statements that you can use for Rails application testing.
In Rails, every model and controller created using the script/generate model, script/generate controller and script/generate scaffold creates corresponding test stubs in the test subdirectory. When using these test stubs, "require 'test/unit'" does not need to be specified.
Assertions verify pre-conditions and post conditions for an application or module to ensure a valid state. The Test::Unit assertion follows a basic pattern, whereby the first parameter is the result expected and the second parameter is the actual result. When the expected and actual value don't match, a message is generated explaining what went wrong.
Go to Test::Unit::Assertions for a list of the core Ruby assertions.
There are also a number of available assertions for Rails developers to use. Go to Module Test::Unit::Assertions Information for information regarding this.
Assertions are used to ensure valid states, as previously mentioned. So, what defines valid state?
In most part, a valid state is controlled by the validation in our code.
Input data can be validated before it even gets stored in the database. To do this, you can define custom validations or use a set of standard validations. For standard validations like checking the name, length and format of an attribute, we can make use of validations provided by Active Record.
Adding validation methods to a model causes validation to occur before the object is saved into the database. If validation passes, the object is written to the database. If validation fails, the error message associated with the failing validation method for the object is added to an error list. This list can be displayed to help the user take appropriate action to resolve the failures.
This article will show examples of how to validate a model using Active Record's set of validators.
For our Team Room example, say we wanted the following validations for a subject:
- All associated document objects to the subject are valid objects
- The subject name is not empty
- The subject name is not more than 20 characters in length
To ensure we check for the above cases, we can make use of some standard validation methods. We add the following validations to app\models\subject.rb:
Listing 10. Validations in app\models\subject.rb
validates_associated :documents
validates_associated :subscriptions
validates_presence_of :name
validates_length_of :name,
:maximum => 20, # maximum 20 characters
:too_long => "is too long, maximum 20 characters"
validates_numericality_of :size, # value is numeric and only integer accepted
:only_integer => true,
:greater_than => 0
validates_presence_of :description
validates_length_of :tag,
:maximum => 10, # maximum 10 characters
:too_long => "is too long, maximum 10 characters"
|
The "validates_associated" method will check that all associated documents with the subject are valid. The "validates_presence_of" method will check that the subject name is not empty.
To check that the subject name length is not more than 20 characters, we use the "validates_length_of" method. We use the "maximum" configuration option to indicate the maximum size of the subject name. The "too_long" option indicates the error message that is generated if the subject name is over 20 characters in length, and is used in place of the "message" option. By default, the "too_long" message is "is too long (maximum is %d characters)".
Subject input data will now be validated according to the rules set above. If any user enters problematic data, we want to ensure that the list of error messages is displayed so that the user can correct the input data.
There are ActionView methods that can help:
- error_message_on - returns the error string for the specified attribute of the object
- error_messages_for - returns an error list for the specified object(s)
Find more information on the methods here.
In our Team Room example, if we want to list all input data error messages entered for a new subject, then we can add the following to the top of app\views\subjects\_form.rhtml.
Listing 11. Input data error messages
<%= error_messages_for 'subject' %> |
The method call will return a div with all error messages (if any exist), so that one can be easily displayed:
Figure 1. Error message when entering a subject name that is too long
You can find a list of the Active Record model validations that can be defined in the class scope here.
Now that we have the subject model validation in place, we can actually test the above validation using an assertion.
We can now utilize assertions to test models associated with our SUBJECTS table. One of the models associated with the SUBJECTS table ensures that the table is populated with necessary information with valid lengths. These validations are important to avoid putting an unnecessary load on the backend database. The validations which will be tested are outlined in \app\models\subject.rb (please refer to Listing 10).
The "test_invalid_with_empty_values" method is a simple example, which demonstrate asserting error conditions with empty values. In the "test_invalid_with_empty_values" method, we test whether empty subject values result in error.
Listing 12. Test method "test_invalid_with_empty_values"
require File.dirname(__FILE__) + '/../test_helper'
class SubjectTest < Test::Unit::TestCase
# Tests validation failure using empty values
def test_invalid_with_empty_values
subject = Subject.new
assert !subject.valid?
assert subject.errors.invalid?(:name)
assert subject.errors.invalid?(:size)
assert subject.errors.invalid?(:description)
end
...
|
Now, let's add a second test to assert valid subject data and a third test to see whether the expected error message is encountered when we specify an incorrect TAG length.
The following three test methods should now be present in test/unit/subject_test.rb:
Listing 13. Test methods for subject_test.rb
require File.dirname(__FILE__) + '/../test_helper'
class SubjectTest < Test::Unit::TestCase
# Tests validation failure using empty values
def test_invalid_with_empty_values
subject = Subject.new
assert !subject.valid?
assert subject.errors.invalid?(:name)
assert subject.errors.invalid?(:size)
assert subject.errors.invalid?(:description)
end
# Tests validation using valid data
def test_valid_subject
subject = Subject.new(:name=>"Perl",
:size=>2,
:description=>"test",
:tag=>"123456789")
assert subject.valid?
end
# Tests invalid TAG length
def test_invalid_tag_length
subject = Subject.new(:name=>"AJAX",
:size=>3,
:description=>"test",
:tag=>"abcdefghijklm")
assert !subject.valid?
assert_equal "is too long, maximum 10 characters",subject.errors.on(:tag)
end
end
|
The successful outcome of the above unit test should result in following output:
Listing 14. Running a subject unit test
D:\rails\teamroom>ruby test/unit/subject_test.rb Loaded suite subject_test Started ... Finished in 13.891 seconds. 3 tests, 7 assertions, 0 failures, 0 errors |
All three tests in the output refer to three test methods defined in subject_test.rb file, and seven assertions refer to all the assert statements in the file. As previously mentioned in Note 3, "." refers to successful result.
The above subject_test.rb example contains seven assertions as outlined in Listing 15 below.
Listing 15. Assertions in subject_test.rb
assert !subject.valid? assert subject.errors.invalid?(:name) assert subject.errors.invalid?(:size) assert subject.errors.invalid?(:description) assert subject.valid? assert !subject.valid? assert_equal "is too long, maximum 10 characters",subject.errors.on(:tag) |
And there are three tests in subject_test.rb as shown in Listing 16.
Listing 16. Tests methods in subject_test.rb
test_invalid_with_empty_values test_valid_subject test_invalid_tag_length |
Instead of manually entering values in the browser to test your Rails application, you can use a test fixture to load sample data into your database tables. Fixtures allow you to populate your test database with predefined data values before you run your tests. Fixtures can be in two different formats: YAML or comma-separated value (CSV) format. Since fixtures are database independent, you can use the same fixture to test your Rails application against different database systems.
We'll use fixtures to test subjects and subscriptions in our Team Room application. For our test, we prefer to use a YAML fixture. To do this, we edit the subjects.yml file in the /test/fixtures directory. An empty subjects.yml file is created by Rails when it generates the corresponding subject unit test. When you run ruby script/generate model to create a new model, or ruby script/generate scaffold to generate a scaffold, fixture stubs will be automatically created and placed in the /test/fixtures directory.
We edited subjects.yml and populated it with 24 different subjects. You can find /test/fixtures/subject.yml in the sample code provided in the Downloads section of this article. Each fixture has a name, followed by a list of colon-separated key-value pairs.
Sample contents of subjects.yml:
Listing 17. Sample contents of subjects.yml
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html photography: id: 1 name: Photography size: 0 description: Digital Photos, images tag: portraits gardening: id: 2 name: Gardening size: 0 description: Gardening info tag: flowers golf: id: 3 name: Golf size: 0 description: Golf information, golf club, golf course discount available to members tag: golf tennis: id: 4 name: Tennis size: 0 description: Tennis information, tennis club, tennis court bookings tag: tennis <etc ..> |
The Rails fixture loading mechanism is already in place so you do not have to add additional code to tell Rails to load the test data. We modified /test/unit/subject_test.rb from Listing 13 and added additional code to test with fixtures.
Listing 18. Fixture-related entries in/test/unit/subject_test.rb
require File.dirname(__FILE__) + '/../test_helper'
class SubjectTest < Test::Unit::TestCase
fixtures :subjects
# count the number of subject fixtures
def test_subject_fixtures
assert_equal 24, Subject.count
end
...
# Previous test methods and assertions are listed below
...
end
|
The fixtures method automatically loads the fixture corresponding to the
given model name at the start of each test method in the test case.
By default, :subjects tells Rails to load the sample
data from the subjects.yml file. We then added an additional "test_subject_fixtures" method to test that all 24 subjects are loaded correctly.
To run the test, issue ruby test/unit/subject_test.rb. The output is shown below:
Listing 19. Subject unit test output using fixtures
D:\rails\teamroom>ruby test/unit/subject_test.rb Loaded suite test/unit/subject_test Started .... Finished in 3.15 seconds. 4 tests, 8 assertions, 0 failures, 0 errors |
Assuming you have started the WEBrick server and logged in as a registered user, now browse to http://localhost:3000/subjects/list. You will notice that 24 subjects are now available for subscription.
Figure 2. Screenshot of populated subject entries
Sometimes you may also need to use dynamic fixtures with ERb (embedded Ruby) to generate fixture data.
One very common example is to generate the actual timestamp as you run your tests. Using
<% ... %> allows you to execute Ruby code, and using
<%= ... %> allows you to execute Ruby code as well as displaying the results.
You will see how to use dynamic fixtures and ERb when we show you how to perform functional tests and performance tests in the upcoming sections.
Sharing fixtures and testing many-to-many associations
We can go one step further and use fixtures to test the associations between subjects and subscriptions. Of course, you can browse to http://localhost:3000/subjects/list and check off a number of subjects to which you want to subscribe as the current logged-in user, and then test the Team Room subject-subscription association. However, this task can become quite tedious if you want to test many user subscriptions that involve a large number of subjects. This would mean you have to manually create a number of user subscriptions, then log in as each user, and check off the subjects to which each user wants to subscribe.
The more efficient way to test a many-to-many relationship between models is to create a new fixture that contains data in the join table. As you may recall in Part 2, we created the SUBJECTS_SUBSCRIPTIONS table to represent the many-to-many relationship between the SUBJECTS and SUBSCRIPTIONS table, and to ensure that the database is normalized. Let's edit the /test/fixtures/subjects_subscriptions.yml file to populate data into the SUBJECTS_SUBSCRIPTIONS table:
Listing 20. subjects_subscriptions.yml
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html # Refer to /test/fixtures/subjects.yml for more information # Subject ID 2 = Gardening subscription160_gardening: subscription_id: 160 subject_id: 2 # Subject ID 3 = Golf subscription160_golf: subscription_id: 160 subject_id: 3 |
The above means that the user whose subscription ID is 160 has subscribed to two subjects: Gardening and Golf.
Testing the subject and subscription association means we must access both the data in the SUBJECTS and the SUBSCRIPTIONS table, as well as the join table SUBJECTS_SUBCRIPTIONS. Therefore, in the /test/unit/subjects_subscription_test.rb test, we want to load all three fixtures.
Listing 21. Fixtures
class SubjectsSubscriptionTest < Test::Unit::TestCase fixtures :subjects_subscriptions, :subjects, :subscriptions [...] end |
This shows that fixtures are very powerful when you need to share test data across multiple test cases. For more information about fixtures, see Class Fixtures in the Ruby on Rails documentation.
Mocks are useful when you want to test the core functionality of the application without worrying about accessing external systems. Although we do verify that the email address is in the correct format when a user registered with the Team Room, we have no idea whether the email address provided in fact exists. The only way for us to confirm that the user email is in fact valid would be to send an email to the supplied email address. If the email address is not valid or does not exist, we expect to get an error from the email provider. To test this failed subscription notification delivery scenario, we can use a mock.
For example, we can mock out the email delivery method in our testing environment. We create an email_provider.rb file in the /test/mocks/test directory that defines the delivery method we want to mock out. With a mock file in place, Rails will load the /test/mocks/test/email_provider.rb first, and then look in /app/models/email_provider.rb.
The /test/mocks/test/email_provider.rb will look like this:
Listing 22. Fixtures
require 'models/email_provider'
class EmailProvider
def deliver(request)
#Mock method
:success
end
end
|
Mocks allow us to concentrate testing on the core functionality in our Team Room application without worrying about inaccessible external resources.
Functional tests: testing controllers
Testing the Rails application controllers aims to ensure all requests coming from the Web browser are answered as expected, based on the user input, while producing the appropriate state changes. So for each action in a controller, there needs to be at least one test case which would exercise that specific path (URL), and sometimes more than one test can account for different states (for instance, accessing an action while the user is authenticated or not).
For a taste of functional testing specifics, we consider the case of the UsersController involved in the
login and registration actions. An associated UsersControllerTest was generated along with the model and
controller classes, and it already contains a complete template for setup and also stub methods that only
expect minimal customization to handle the execution paths.
Listing 23. Users controller test
require File.dirname(__FILE__) + '/../test_helper'
require 'users_controller'
# Re-raise errors caught by the controller.
class UsersController; def rescue_action(e) raise e end; end
class UsersControllerTest < Test::Unit::TestCase
fixtures :users
def setup
@controller = UsersController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@first_id = users(:first).id
end
[...]
end
|
An essential method, the setup initializes the three instance variable always required for the functional test:
the controller to be tested, the request containing the user input along with HTTP header information,
and the response packing the data rendered from templates along with status codes.
A key aspect of testing the controller is the associated fixture providing the test data for our scenario, and this specific controller gives us a chance to understand the usage of some dynamic aspects in fixtures. In the test case above, featuring an authenticated user, there is a need to populate the USERS table with a valid entry to compute the encrypted password. Although the encrypted password could be calculated separately and just used directly in the fixtures, the template substitution used in views and partials works similarly, simplifying our task.
Listing 24. Users fixture
<% SALT = "tstsalt" unless defined?(SALT) >
first:
id: 141
usertype: usr
firstname: Homer
lastname: Simpson
extension: '9955'
email: homer@simpson.org
userid: homers
salt: <%= SALT %>
hash_passwd: <%= User.encrypt('secret' , SALT) %>
|
The initial test_index test case would need to be split into two different
test cases that would allow a more precise verification of request handling when the user is authenticated, as opposed to not being authenticated:
Listing 25. Test index action in Users controller
def test_index_without_user
get :index
assert_redirected_to :action => "login"
assert_equal "Please sign-in" , flash[:notice]
end
def test_index_with_user
get :index, {}, {:user_id => users(:first).id}
assert_response 302
end
|
To exercise the index action for this controller, which would invariably end up on the login page
if the user is not yet authenticated, the test case needs to simulate the GET request from the browser.
To generate an HTTP request, the get method defined by the ActionController::Integration::Session takes the URL,
a hash representing HTTP parameters passed with the action (empty in the case above), and a hash of parameters
to populate the session. Now we just need to invoke the specific test case:
Listing 26. Run users controller test
D:\rails\teamroom>ruby test\functional\users_controller_test.rb -n test_index_with_user Loaded suite test/functional/users_controller_test Started . Finished in 0.719 seconds. 1 tests, 1 assertions, 0 failures, 0 errors |
Testing email notification and subscription confirmation
Testing email notification involves both unit testing and ensuring the generated email content verification,
but also functional testing which will make sure the email notification is always sent out when expected.
Based on the email notification controller implemented in Part 2
based on the Action Mailer, we should
locate the stub functional test generated SubscriptionMailerTest and take advantage of the fixtures already
created for the models involved in the notification action.
For unit testing email notification and subscription confirmation, the mailer test drive requires you to configure
ActionMailer for local email delivery (delivery_method = :test), and also to use the model fixtures to setup the
objects required to call the notification and confirmation.
Listing 27. Mailer functional test setup
class SubscriptionMailerTest < Test::Unit::TestCase
FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
CHARSET = "utf-8"
include ActionMailer::Quoting
fixtures :users
fixtures :subscriptions
fixtures :subjects
fixtures :documents
def setup
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@user = users(:first)
@subscription = subscriptions(:one)
@doc = documents(:first)
@subject = subjects(:ruby_on_rails)
@subject.documents << @doc
@subject.subscriptions << @subscription
end
[...]
end
|
The test implementation then uses the mailer class to create the email content (create_notify),
but not actually to send to an SMTP server, and through the usage of assertions verifies the generated
parts of the message (for instance, recipients, sender, timestamp, and so on) including certain sections of the body.
Listing 28. Notification email test
def test_notify
response = SubscriptionMailer.create_notify(@doc)
assert_equal("TeamRoom new shared document notification", response.subject)
assert_equal("teamroom@developerWorks.ibm.com", response.from[0])
assert_equal(nil, response.to)
assert_equal("homer@simpson.org", response.bcc[0])
assert_in_delta(Time.now, response.date, 1.0)
assert_match(/A new document DB2onRails-logo.gif/, response.body)
assert_match(/subject: Ruby on Rails/, response.body)
end
|
A similar approach is being used to test the subscription confirmation email while creating and then
verifying the content of the message. Furthermore, the functional testing of the emailer implies a controller
test case which will not deliver emails, but rather append email messages to an in-memory array
(ActionMailer::Base.deliveries = []). This allows for verifying the number of emails sent out, and also for validating the content of each message.
Although such a template is not generated by default in the list of test directories outside of unit, functional and integration, developers could be interested in estimating some application performance characteristics. While focusing on performance too early in the development is never wise, by the time the development process reaches the end cycles, some performance scenarios are recommended to address capacity planning and to ensure some regression testing when things start to get slow in subsequent iterations.
For a glimpse into how performance testing can be implemented along the other test categories, let's consider a scenario that might amount to an excessive load on the Team Room application if deployed in a relatively large enterprise environment. We start as before with some test data, and for that we will provide a set of fixtures that only get loaded for performance testing, to localize when a relatively large amount of test date is initialized.
Listing 29. Another dynamic user fixture for large data test set
<% SALT = "tstsalt" unless defined?(SALT) %>
<% 200.upto(1200) do |i| %>user_<%= i %>:
id: <%= i %>
usertype: usr
firstname: New<%= i %>
lastname: One
extension: '9999'
userid: new<%= i %>
email: new<%= i %>@domain.net
salt: <%= SALT %>
hash_passwd: <%= User.encrypt('secret' , SALT) %>
<% end %>
|
We can consider the performance test to be based on a regular usage scenario, and extend to the large data test set imposing some extreme loads. That implies the test controller is
fairly similar to that used for regular functional testing, but the test case is exaggerating a certain aspect.
Let's consider the user controller and the login action to be tested on extreme loads. Except from where the test data is loaded (self.fixture_path), the test case setups should be identical.
Listing 30. User login performance test setup
class UserLoginTest < Test::Unit::TestCase
self.fixture_path = Pathname.new(File.dirname(__FILE__)).parent +
'fixtures' + 'performance'
fixtures :users
def setup
@controller = UsersController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
[...]
end
|
Now, to test the Team Room capabilities in handling a large number of simultaneous users, we can attempt to login all users defined in the fixture at once, and maybe even impose some minimum time limits for these actions. In Listing 31, we have implemented that in a loop, while making sure the logger is turned off, in order to collect time estimates using the Ruby standard library benchmark:
Listing 31. Test user controller performance during login action
def test_1000_login
@controller.logger.silence do
user_count = 0
elapsed_time = Benchmark.realtime do
1200.downto(200) do |id|
user = users("user_#{id}")
post :login, :userid => user.userid, :password => 'secret'
user_count += 1
end
end
assert_equal 1001, user_count
assert elapsed_time < 10.0
end
end
|
A more realistic performance scenario might involve some of the XML queries that stand behind the marketing reports. We shall leave that as an exercise for readers interested in moving this or other similar applications to production. An important aspect of performance testing implementation is the usage of a regular functional and integration test case in connection with large test data sets and extensive loads. This approach makes the implementation of such performance regression harnesses quite easy and very effective.
Running application tests using Rake
When we started to build the Team Room application using the rails teamroom command, a directory structure was generated, and even more importantly, a set of utilities built into the Rails gem were "hooked". The Rails gem is quite a thin and simple component that orchestrates behind the scenes the seamless integration of all other components; two of the most visible being Active Record and Action View. At the same time, the Rails gem addresses a set of common concerns across all Web applications using the framework through the powerful Rake gem helper.
Rake, the Ruby equivalent of the make utility, through its predefined task (the equivalent of make targets)
not only ensures simple shortcuts to migration, schema load/dump, Rails gems update/freeze, and documentation
creation, but also for testing. Running rake --tasks in the Team Room application root directory, one can get
acquainted with the entire set of predefined tasks provided by the reliable assistant, rake. It all gets
infused into the Team Room application from the very beginning, when the directory structure is created and
the Rake file is created, the tasks/rails library path containing the set of predefined tasks is then shared.
With a simple rake test command, all unit and functional tests built so far can be executed together, and can easily be invoked during the development cycle. The tests can also be run based on category, and the Rails application standard directory structure makes it easy to load all defined test in the test/unit path as in Listing 32:
Listing 32. Running all unit tests for Team Room application
D:\rails\teamroom>rake test:units (in D:/rails/teamroom) D:/ruby/bin/ruby -Ilib;test "D:/ruby/lib/ruby/gems/1.8/gems/rake-0.7.2/lib/rake/rake_test_loader.rb" "test/unit/customer_info_test.rb" "test/unit/document_test.rb" "test/unit/subjects_subscription_test.rb" "test/unit/subject_test.rb" "test/unit/subscription_mailer_test.rb" "test/unit/subscription_test.rb" "test/unit/user_test.rb" "test/unit/xml_content_test.rb" Loaded suite D:/ruby/lib/ruby/gems/1.8/gems/rake-0.7.2/lib/rake/rake_test_loader Started .......... Finished in 11.553 seconds. |
The same generic test loading occurs while calling rake test:functionals,
while rake chains such test execution, to cover the entire set.
DB2 Rails application problem determination
As you develop your Rails application, you may observe unexpected results or errors. The first thing to check are the log files in the /log directory (in our example, d:\rails\teamroom\log). Rails writes logs and errors for each environment in its own log. For problems in the development environment, look at development.log, for problems in the test environment, look at test.log. In addition to the Rails log files and diagnosing problems in the application itself, DB2 trace utilities and logs can shed further light in diagnosing Rails application problems.
As mentioned in Part 1 of our series, the ibm_db driver utilizes the IBM driver for Open Database Connectivity (ODBC) and Call Level Interface (CLI) to connect to IBM data servers. This means that all interactions with IBM data servers will go through IBM driver for ODBC and CLI, and can be captured using the CLI trace. A CLI trace captures all of the API calls made by the Ruby application to the DB2 driver for ODBC and the CLI driver, and logs all input parameters as well as values returned from the driver to the application. Therefore, the CLI trace provides valuable insight into input and output values as well as actual SQL statements generated by the Rails framework that occur behind the scene.
Steps for enabling a CLI trace can be found at the Infocenter.
A CLI trace is a valuable tool to verify correct SQL statements are being issued by the application and correct values are returned from the database. As most of the SQL statements are generated by Rails in our Team Room application, following CLI trace output shows the query sent to the DB2 database by uploading the XML document as shown in Figure 2. Uploading the XML document involves inserting data into three separate tables.
Inserting the content type, name, platform, size, subject and id of the user who uploaded the file as well as updated and created time to documents table.
Listing 33. CLI trace: Part 1SQLExecDirect( hStmt=1:8 ) ---> Time elapsed - +1.844000E-003 seconds ( pszSqlStr="INSERT INTO documents (content_type, name, platform, size, updated_at, subject_id, user_id, created_at, data) VALUES('text/xml', 'CAN-Central.xml', 'Any', 125177, '2007-06-07 13:12:57', NULL, 100, '2007-06-07 13:12:49', NULL)", cbSqlStr=225 ) ( StmtOut="INSERT INTO documents (content_type, name, platform, size, updated_at, subject_id, user_id, created_at, data) VALUES(?, ?, ?, 125177, ?, NULL, 100, ?, NULL)" ) ( Package="SYSSH200 ", Section=11 ) sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 349 ) sqlccsend( ) rc - 0, time elapsed - +1.991000E-003 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 188 ) - rc - 0, time elapsed - +2.471000E-003 ( Row=1, iPar=1, fCType=SQL_C_CHAR, rgbValue="text/xml" - x'746578742F786D6C', pcbValu... ( Row=1, iPar=2, fCType=SQL_C_CHAR, rgbValue="CAN-Central.xml" - x'43414E2D43656E74726... ( Row=1, iPar=3, fCType=SQL_C_CHAR, rgbValue="Any" - x'416E79', pcbValue=3, piIndicato... ( Row=1, iPar=4, fCType=SQL_C_CHAR, rgbValue="2007-06-07 13:12:57" - x'323030372D30362... ( Row=1, iPar=5, fCType=SQL_C_CHAR, rgbValue="2007-06-07 13:12:49" - x'323030372D30362... sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 223 ) sqlccsend( ) rc - 0, time elapsed - +2.380000E-004 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 127 ) - rc - 0, time elapsed - +1.909000E-003 SQLExecDirect( ) <--- SQL_SUCCESS Time elapsed - +1.504370E-001 seconds ...Inserting the name of the XML file, corresponding id from documents table and XML placeholder.
Listing 34. CLI trace: Part 2SQLExecDirect( hStmt=1:8 ) ---> Time elapsed - +5.003000E-003 seconds ( pszSqlStr="INSERT INTO xml_contents (name, document_id, data) VALUES('CAN-Central.xml', 101, '<ibm>@@@IBMXML@@@</ibm>')", cbSqlStr=108 ) ( StmtOut="INSERT INTO xml_contents (name, document_id, data) VALUES(?, 101, ?)" ) ( Package="SYSSH200 ", Section=11 ) sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 261 ) sqlccsend( ) rc - 0, time elapsed - +1.799000E-003 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 137 ) - rc - 0, time elapsed - +1.865000E-003 ( Row=1, iPar=1, fCType=SQL_C_CHAR, rgbValue="CAN-Central.xml" - x'43414E2D43656E747261... ( Row=1, iPar=2, fCType=SQL_C_CHAR, rgbValue="<ibm>@@@IBMXML@@@</ibm>" - x'3C69626D3E40... sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 221 ) sqlccsend( ) rc - 0, time elapsed - +1.000000E-005 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 89 ) - rc - 0, time elapsed - +1.840000E-003 SQLExecDirect( ) <--- SQL_SUCCESS Time elapsed - +8.456800E-002 seconds ...This is followed by an update statement to actually insert XML data.
Listing 35. CLI trace: Part 3SQLPrepare( hStmt=1:8 ) ---> Time elapsed - +1.861000E-003 seconds ( pszSqlStr="UPDATE xml_contents SET (data) = (?) WHERE id = 101", cbSqlStr=51 ) ( StmtOut="UPDATE xml_contents SET (data) = (?) WHERE id = 101" ) ( Package="SYSSH200 ", Section=11 ) sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 244 ) sqlccsend( ) rc - 0, time elapsed - +1.000000E-005 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 120 ) - rc - 0, time elapsed - +1.923000E-003 SQLPrepare( ) <--- SQL_SUCCESS Time elapsed - +3.831300E-002 seconds ... SQLDescribeParam( hStmt=1:8, usPar=1, psSQLType= ... ---> Time elapsed - +2.315000E-003 seconds SQLDescribeParam( psSQLType=SQL_XML, puiParamDef=0, psScale=0, ... <--- SQL_SUCCESS Time elapsed - +2.457500E-002 seconds SQLBindParameter( hStmt=1:8, iPar=1, fParamType=SQL_PARAM_INPUT, fCType=SQL_C_BINARY, fSQLType=SQL_XML, cbColDef=0, ibScale=0, rgbValue= ... ---> Time elapsed - +2.225000E-003 seconds SQLBindParameter( ) <--- SQL_SUCCESS Time elapsed - +3.743500E-002 seconds SQLExecute( hStmt=1:8 ) ---> Time elapsed - +1.848000E-003 seconds E0D0A202020202020202020203C2F6974656D3E0D0A202020....., pcbValue=125177 ) sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 61440 ) sqlccsend( ) rc - 0, time elapsed - +2.710000E-004 sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 61440 ) sqlccsend( ) rc - 0, time elapsed - +2.120000E-004 sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 2450 ) sqlccsend( ) rc - 0, time elapsed - +1.300000E-005 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 89 ) - rc - 0, time elapsed - +2.345000E-003 SQLExecute( ) <--- SQL_SUCCESS Time elapsed - +9.107600E-002 seconds ...Lastly, the SUBJECTS table is populated accordingly, using an insert statement.
Listing 36. CLI trace: Part 4SQLExecDirect( hStmt=1:10 ) ---> Time elapsed - +2.166000E-003 seconds ( pszSqlStr="INSERT INTO subjects (name, size, tag, description) VALUES('Marketing', 1, 'sales', '@@@IBMTEXT@@@')", cbSqlStr=100 ) ( StmtOut="INSERT INTO subjects (name, size, tag, description) VALUES(?, 1, ?, ?)" ) ( Package="SYSSH200 ", Section=14 ) sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 263 ) sqlccsend( ) rc - 0, time elapsed - +1.920000E-004 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 154 ) - rc - 0, time elapsed - +1.823000E-003 ( Row=1, iPar=1, fCType=SQL_C_CHAR, rgbValue="Marketing" - x'4D61726B6574696E67', ... ( Row=1, iPar=2, fCType=SQL_C_CHAR, rgbValue="sales" - x'73616C6573', pcbValue=5, ... ( Row=1, iPar=3, fCType=SQL_C_CHAR, rgbValue="@@@IBMTEXT@@@" - x'40404049424D5445 ... sqlccsend( Handle - 84681200 ) sqlccsend( ulBytes - 182 ) sqlccsend( ) rc - 0, time elapsed - +3.160000E-004 sqlccrecv( timeout - +0.000000E+000 ) sqlccrecv( ulBytes - 89 ) - rc - 0, time elapsed - +2.128700E-002 SQLExecDirect( ) <--- SQL_SUCCESS Time elapsed - +1.243580E-001 seconds
A DB2 trace captures all traceable internal DB2 function calls and are an invaluable tool to investigate application problems involving DB2. It provides information regarding DB2 internal activities at the time of application execution. There are several characteristics of a DB2 trace which allow for effective information gathering.
- The trace can be turned on and off dynamically. The trace can be enabled and disabled without recycling the application. This means that if the exact point of failure is known, the trace can be enabled just prior to the incident to gather only relevant information.
- DB2 trace information can be stored in memory or disk.
- You can apply trace mask to trace out only specific components.
For instructions on enabling a DB2 trace, please refer to the Infocenter.
When troubleshooting a DB2 issue with Rails applications, it is useful to gather a DB2 trace with a CLI trace (together) on the client where application resides.
The db2diag.log and db2diag tool
The db2diag.log logs errors and some warnings encountered by DB2. The db2diag.log can be reviewed to determine if any DB2 errors or warnings were logged during execution of our application. The sensitivity of the log can be changed from the default of 3 to other values from dbm cfg.
The db2diag analysis tool is available for DB2 9® to filter and format the db2diag.log file. It allows for filtering log entries that pertain to specific database or timestamp values among others. For further details regarding the db2diag tool, please refer to the Infocenter.
Frequently asked questions and caveats
- Is a DB2 client installation required in order to use the ibm_db Ruby adapter and driver? Yes, as stated in Part 1 of the DB2 and Ruby on Rails series, the IBM_DB adapter (ibm_db_adapter.rb) has a direct dependency on the ibm_db Ruby driver, which utilizes the IBM driver for ODBC and the CLI to connect to IBM data servers. Therefore, the minimal requirement is the IBM DB2 Driver for ODBC and CLI, but any DB2 9 FP2 or higher client package (which includes the CLI driver) would be sufficient to enable the IBM_DB adapter connection to all supported IBM data servers.
- The following error is observed when trying to run a Rails application against DB2.
SQL0954C: Not enough storage is available in the application heap to process the statement.As stated in Part 1, Rails applications on DB2 9 require a minimum APPLHEAPSZ of 1024. To check APPLHEAPSZ, connect to your specific database and retrieve the configuration parameters. - Is DB2 Connect required to access a DB2 i5 or a DB2 for z/OS server by way of IBM Ruby Driver? Yes, DB2 Connect is required in order for a DB2 client to connect to DB2 i5 or DB2 z/OS server. When using the IBM DB2 driver for ODBC and CLI, a valid license file is required in the driver install path.
- Getting
rake abortederrors when trying to clone a test environment with development using rake db:test:* commands. Check database.yml file. The test environment needs to be setup according to database.yml specifics as provided in the default configuration generated by the rails command. Also, make sure you obtain the two fixes as stated in Note 1. - Encountering problems that are known to be fixed in the latest version of the IBM_DB adapter.
However, the gem list --local command shows that the latest
version of the IBM_DB adapter is installed.
Check to make sure you do not have a copy of ibm_db_adapter.rb in
<ruby_path>\lib\ruby\gems\1.8\gems\activerecord-1.15.3\lib\active_record\connection_adapters
or a similar path on UNIX®.
There should only be one copy of ibm_db_adapter.rb in the latest IBM_DB gem installed in the
GEM_HOMEpath <ruby_path>\lib\ruby\gems\1.8\gems\ibm_db-<version>-mswin32\lib\active_record\connection_adapters (or a similar path on UNIX). This is the only IBM_DB adapter that is loaded in the Rails environment. - For the upcoming Rails 1.2.4,
config.connection_adaptersandRAILS_CONNECTION_ADAPTERSwill be eliminated. Therefore, there will be no further need to register "ibm_db" in the list of connection adapters in the Rails framework through the manual step described in Part 1. Once the IBM_DB adapter is installed using the gem install ibm_db command, it will be immediately available for the Rails environment to find and load.
The built-in test support in the Rails framework makes testing easy! Each Rails application has a test, development and production environment defined in the config/database.yml file that allows you to set up different databases for various purposes. When you initially create a new Rails project, Rails generates a test infrastructure for you. For every model and controller you create, corresponding test stubs are created. Unit tests test the Rails models, while functional and integration tests ensure the Rails application works as designed at a higher level. Fixtures allow you to specify data for testing, and finally, mock objects let you concentrate on testing the core application without worrying about accessing network connections or external systems. These features are built in to Rails, making it exceptionally convenient to test.
| Description | Name | Size | Download method |
|---|---|---|---|
| Team room sample code | Teamroom3.zip | 10KB | HTTP |
Information about download methods
Learn
-
"DB2 and Ruby on Rails, Part 1: Getting started with DB2 and Ruby on Rails"
(developerWorks, May 2007) introduces the Starter Toolkit for DB2 on Rails and various methods for installing the IBM_DB driver, as well as Rails migration with DB2.
-
"DB2 and Ruby on Rails, Part 2: DB2 and pureXML with Ruby on Rails"
(developerWorks, June 2007) shows how DB2's Native XML support of pureXML with Ruby on Rails provides a powerful combination for Web application development.
-
"Ruby on Rails and J2EE:Is there room for both?"
(developerWorks, July 2005) compares two Web application framework: J2EE vs. Ruby on Rails.
-
"Crossing borders: Exploring Active Record"
(developerWorks, March 2006) explores Active Record, the persistence engine behind Ruby on Rails
-
"Crossing borders: What's the secret sauce in Ruby on Rails?"
(developerWorks, May 2006) explores what makes Rails productive and looks at Rails-inspired ideas
that should get more attention within the Java community.
-
"An introduction to Ruby on Rails for DB2 developers"
(developerWorks, June 2006) is step-by-step introductory article to Ruby on Rails.
-
"Crossing borders: Rails migrations"
(developerWorks, August 2006) provides a good overview of Rails migration and shows Rails migration with MySQL.
-
"Make Ruby on Rails easy with RadRails and Eclipse" (developerWorks, September 2006) introduces an Eclipse-based development tool for Ruby on Rails.
- See the ActiveRecord association documentation.
- "From DAD to annotated XML schema decomposition"
by Mayank Pradhan (developerWorks, Apr 2006) is a guide for migrating from XML Extender decomposition to annotated XML schema decomposition.
- "Ruby on Rails and XML"
by Daniel Wintschel (developerWorks, Apr 2007) is a guide to manipulating XML data in Ruby.
- Make the DB2 9 pureXML Guide your DB2 9 pureXML reference.
- Browse the
technology bookstore
for books on these and other technical topics.
-
Visit the developerWorks resource page for DB2 for Linux, UNIX, and Windows to read articles and tutorials and connect to other resources to expand your DB2 skills.
-
Learn about DB2 Express-C, the no-charge version of DB2 Express Edition for the community.
Get products and technologies
- Download
IBM product evaluation versions
and get your hands on application development tools and middleware products from
DB2®, Lotus®, Rational®, Tivoli®, and
WebSphere®.
-
Download a free trial version of DB2 Enterprise 9.
-
Now you can use DB2 for free. Download DB2 Express-C, a no-charge version of DB2 Express Edition for the community that offers the same core data features as DB2 Express Edtion and provides a solid base to build and deploy applications.
-
Download DB2 Developer Workbench and other DB2 9 products Download DB2 Developer Workbench and other DB2 9 products.
- Get the Rails Adapter/Driver for IBM Databases (rubyforge.org).
- Download the IBM_DB adapter/driver gem and plugin.
- Download the Starter Toolkit for DB2 on Rails.
- Download RadRails.
Discuss
- Participate in the discussion forum.
- Visit the alphaWorks Forum for Starter Toolkit for DB2 on Rails for relevant discussions.
- The RubyForge rubyibm Forum welcomes your RubyForge questions and comments.
John Chun is a specialist of the DB2 Advanced Support team working in the area of application development and tooling. He has worked in the IBM DBT Toronto lab for 7 years resolving DB2 application issues with various languages including Java, C, C++, Perl, REXX, C# and others. John has worked on a number of projects involving the DB2 CLI and OLEDB driver, as well as the .NET data provider. John is a DB2 Certified Solutions Expert and Certified Websphere Administrator.
Alex Pitigoi is an advisory software engineer at the IBM Toronto Lab. He has worked on various software development projects in the Information Management since 1998, focusing on Web technologies and database administration. Most recently, he drove the development of the SQLModel project, now incorporated into the Eclipse Data Tools Project, as well as the overall architecture for the database administrative Web Tools across multiple IBM data servers. Alex also worked on the DB2 Satellite Administration Center, the IBM Express Runtime, and lead the development of the first set of Web Tools delivered for DB2. His current focus is IBM's data servers enablement for new open source technologies (Ruby, Python, PHP).
Christine Law is senior DB2 specialist and an IBM Certified Expert at the IBM Toronto Lab, where she is responsible for resolving DB2 applications problems and defects. She has extensive application development experience on Linux, UNIX and Windows platforms with different programming languages and scripting languages, specializing in JDBC, SQLJ, stored procedures and embedded SQL. Her recent interests includes Open Source technologies such as AJAX and Ruby.
Naomi Ngan completed an Honours degree in Computer Science and Statistics at the University of Toronto in Canada in 2000. Upon graduating, she joined IBM, where she was responsible for resolving IBM DB2 RDBMS product defects and problems in an application development environment. After almost 4 years at IBM, she went on to the Ernest Gallo Clinic and Research Center at UCSF to develop Bioinformatics software. This includes designing and developing database objects, JSPs, Java standalone applications and stored procedures within an XML environment on Linux and Windows platforms. Currently, she is a senior software engineer at Autonomy Corporation developing J2EE enterprise software. She has in-depth knowledge of application development and tooling for DB2 and holds numerous IBM and Sun developer certifications in DB2, XML, WebSphere, and Java/J2EE.
Comments (Undergoing maintenance)





