YaST/Web/Development/Testing
From openSUSE
Contents |
Conventions for testing
This document collects conventions for testing the code that conforms the WebYaST product.
Testing of Ruby on Rails Application
See A Guide to Testing Rails Applications for more details.
In short, automatic tests in Ruby on Rails can be divided to three levels:
- unit tests - model testing - low-level tests for the core functionality (see e.g. Unit Testing for more details)
- functional tests - controller testing - test one action
- integration tests - workflow testing - test sequence of several actions
WebYaST also uses UI tests using Selenium framework, see the UI Testing page.
Needed packages
In addition to standard Ruby on Rails packages these additional packages are needed for testing:
- rubygem-test-unit - base testing framework
- rubygem-mocha - library for mocking function calls (to avoid execution of the critical part on the current system and make the tests system independent)
- rubygem-rcov - for checking code coverage (how much code is covered/tested by the tests)
Starting Tests
Simply invoke rake command in a WebYaST directory to start all tests. The command will start all tests which belong to the current directory, if the no test is found it searches in parent directory.
Use test:units, test:functionals or test:integration rake target to start only tests of the requested type (e.g. call rake test:units to run only unit tests). Use rake test TEST=foo_test.rb to start only one test.
WebYaST RPM packages do not contain tests, they are only available in the GIT repository. (The tests are continuously run in the integration server and they are not needed at production server.)
Code Coverage
Use rake test:test:rcov to run all tests with coverage support. At the end it will generate a HTML coverage report and print a link to it. Open the link in a web browser and check the coverage. The main page of the report contains a summary, you can click on a file to see the lines which are not covered by tests. Ideally, all files should have 100% coverage.
Note: neither 100% code coverage guarantees that the code is bug free and perfect! See RCov page for more details.
Writing a Test
Tests are stored in test/{unit|functional|integration} subdirectory depending on the type of the test. The test file name must match *_test.rb pattern to be automatically found by rake.
The following sections describe solving WebYaST specific problems.
YaST REST Service
Code in the REST service has objective to provide a RESTful resource representing data coming from the system.
This data can be retrieved by various ways:
- PackageKit
- YaST YaPI calls via DBus
- Calling command line programs (over DBus SCR execute service, to preserve policies)
- Others
Conventions
Don't access the system
Your tests can't depend on what the system returns. Use the Mocha library to mock the result of classes and test your code against different system scenarios you consider.
Model encapsulation
If you have a UserController which retrieves data from the system users, encapsulate the way the data is retrieved by providing a small ActiveRecord/ActiveResource like API ( User.find, User.save ), this gives you the following advantages:
- The controller looks like any Rails controller
- Testing the controller without going to the system requires you to mock the User class data which is simpler than mocking the YaPI calls.
- You need to test any layer of abstraction you create, so you need to test the YaPI calls anyway.
For example, if you have a SoftwareService resource class, which reads the product files in /etc/zypp/services.d and exposes them as a RESTful resource, you could create an index controller like:
def show
# get all files
Dir.glob("/etc/zypp/services.d/*").each ...
# parse ini file, make a hash object
hash = INI.parse(files)
render hash.to_xml
end
You could test the controller in the controller test class by stubbing what Dir returns when called with that directory, and what File reads when called with those paths.
Dir.stubs(:glob).with("/etc/zypp/services.d/*).returns(filelist)
# stub File.read to return a hardcoded ini file
...
See how it gets complicated? You really don't want that in the controller test. Because the controller test should be focused in the workflow and not in grabbing the data. You should test how the controller *reacts* to certain data and exceptions. Instead, create a SoftwareService model, with the typical find, save, etc methods, and allow to construct it from its attributes.
Then in the controller class, mock
s1 = SoftwareService.new(attrs) s2 = SoftwareService.new(attrs) SoftwareService.stubs(:find_all).returns([s1, s2])
And now you can test your controller without worrying how the data is actually read. You can also mock SoftwareService to throw exceptions on certain values and see how your controller reacts.
Then you need to create a new test for the SoftwareService class. In this test you can mock the Dir class to return a fixed filename list when Dir.glob is called with the services.d path.
Then you need to make the decision if you mock the File class to return something fix when constructed with one of those fixed paths. For example you can mock File.new to return an StringIO object constructed from some fixed INI-style string (which acts like a normal file, as it also inherits IO), but only when called with the fixed paths you know.
Dir.stubs(:glob).with("/etc/zypp/services.d/*").returns([ "/etc/zypp/services.d/service1.service", "/etc/zypp/services.d/service2.service"])
Dir.glob("/etc/zypp/services.d/*")
=> ["/etc/zypp/services.d/service1.service", "/etc/zypp/services.d/service2.service"]
The file does not exist, so any attempt to open it will fail.
File.new("/etc/zypp/services.d/service2.service")
Errno::ENOENT: No such file or directory - /etc/zypp/services.d/service2.service
We can stub File.new to return a String based IO object, if we know that our code will only do operations like read.
File.stubs(:new).with("/etc/zypp/services.d/service2.service").returns(StringIO.new("[service]\nattr1=val1\n"))
Now you can read the file even if it does not exist, you will be dealing with a StringIO object, constructed from a fixed string we want to test with:
f = File.new("/etc/zypp/services.d/service2.service")
=> #<StringIO:0x7fa223f10a28>
f.read
=> "[service]\nattr1=val1\n"
But then we will need to mock File for every SoftwareService method that reads the files. Instead, you can move the reading code to a separate method content_for(path), mock that one to return a fixed INI String when testing any method of SoftwareService, and additionally add a test to test content_for(path) that will be the only test mocking the file. See the pattern?
- You have various methods that to be tested require to mock something complex, like ComplexClass
- You move the code depending on File to a separate method read_data (fictional name) which talks to ComplexClass
- You test all methods by mocking the read_data method to return a simple String, Array, etc.
- You add an additional test to the testcase that tests the new read_data method, and only here you mock ComplexClass
That way you simplify your testcase without reducing coverage.
YaST Web Client
The Web Client part has similar problems as the Web Service part regarding to automatic testing.
Do not connect to a Web Service
The Web Client also should not access the system - in this case it means it should not use a real Web Service (REST) service. There are two possibilities:
- Mock the REST system calls
- Fake REST responses using e.g. FakeWeb plugin
Here is an example of mocking REST calls:
# override the WebYaST proxy
class Proxy
attr_accessor :result, :permissions, :timeout
def find
return result
end
end
# a part of the result
class Action
attr_accessor :my_attribute
def initialize
@active = false
end
end
# REST result
class Result
attr_accessor :foo, :bar
def fill
@foo = Action.new
@bar = Action.new
end
def save
return true
end
end
def setup
# create a testing request
@request = ActionController::TestRequest.new
# create a REST result and fill it with fake data
@result = Result.new
@result.fill
# create a WebYaST proxy
@proxy = Proxy.new
@proxy.permissions = { :read => true, :write => true }
@proxy.result = @result
# register the proxy fake
YaST::ServiceResource.stubs(:proxy_for).with('org.opensuse.foo_servce').returns(@proxy)
# create the controller for testing
@controller = FooController.new
# skip the login check
FooController.any_instance.stubs(:login_required)
end
# here define the tests
...

