This is the documentation for Object Oriented approach used in automated UI tests for SUSE products.
- Overview
The Test Framework is based on Page Object Desing Pattern with the certain adaptation related to the environment-specific demands.
It is broken on several Layers. The interactions between the layers could be represented with the following diagram.
main.pm is an entry point for all the tests in openQA, the distribution is set here with DistributionProvider.
use testapi;
...
testapi::set_distribution(DistributionProvider->provide());
DistributionProvider is a factory that returns the required Distribution depending on openQA environment variables ('VERSION', 'ARCH', 'BACKEND' etc.). Currently, Tumbleweed is returned as the default one, following "Factory First" rule.
package DistributionProvider;
...
sub provide {
return Distribution::Sle::15->new() if version_utils::is_sle('15+');
return Distribution::Sle::12->new() if version_utils::is_sle('12+');
return Distribution::Opensuse::Leap::15->new() if version_utils::is_leap('15.0+');
return Distribution::Opensuse::Leap::42->new() if version_utils::is_leap('42.0+');
return Distribution::Opensuse::Tumbleweed->new();
}
main.pm then calls a scheduled Test Module that is using the Distribution to access its components through Controller layer.
Important: Test Module must be inherited from opensusebasetest or one of its children to have an access to the Distribution.
use parent 'opensusebasetest';
sub run {
my $partitioner = $testapi::distri->get_partitioner_controller();
}
Test Module is a layer containing test case steps that need to be executed on the system under test (SUT).
All the data for the test should be provided on this level. Do not provide any test data in Controller or Page layers.
Example:
# Should partitioner enable separate home partition is set in Test Module.
sub run {
my $partitioner = $testapi::distri->get_partitioner();
$partitioner->edit_proposal(has_separate_home => 1);
}
Test Module is not allowed to use os-autoinst testapi functions directly. It should use methods, provided by Controller layer instead. This allows to hide the details of the UI structure and operate with the business logic the system provides.
Test Module is able to interact only with the Controller layer.
Controller is a layer that provides methods to interact with the system under test in business terms.
Do not define any test data in Controller layer as it could make test maintenance more complicated. Use the test data passed from the Test Module layer instead.
Example:
-
If need to use conditions:
sub create_encrypted_partition { my ($self, $is_lvm) = @_; if ($is_lvm) { $self->get_partitioning_scheme_page()->select_lvm_checkbox(); } ... }
-
To make an action depending on a value:
sub create_filesystem { my ($self, $filesystem) = @_; $self->get_filesystem_options_page()->select_filesystem($filesystem); ... }
Ideally, it should not use the os-autoinst testapi directly.
-
Do not use testapi methods that communicates with the SUT (e.g.
send_keys
,assert_screen
). Wrap them into Page methods with the meaningful names instead. -
Using
get_var
to change the flow of a test or get a data for the test should be avoided as much as possible (e.g. to decide whether check or uncheck checkbox, use method parameters instead and pass the data from Test Module.
NOTE: Since os-autoinst API does not have separation between methods for interacting with openQA and interacting with the SUT, the usage of os-autoinst testapi in Controller layer cannot be fully avoided at the moment. So, some testapi functions may be used in exceptional cases.
Example:
It is ok when get_var
is used in combination with
record_soft_failure
to just highlight known issue in openQA.
sub create_encrypted_partition {
...
if (get_var('SOME_VARIABLE')) {
record_soft_failure('bsc#1234567');
return;
}
...
}
It knows only about Pages and hides the complexity of manipulating with them from the Test Module layer.
However, it also provides access to the Pages directly through the getters. This compromising solution was added for the cases when specific and rare actions should be made.
Example:
For instance, there might be a test, that should create an encrypted partition. Positive case may use something like:
sub create_encrypted_partition {
my ($self) = @_;
$self->get_partitioning_scheme_page()->select_enable_disk_encryption_checkbox();
$self->get_partitioning_scheme_page()->enter_password();
$self->get_partitioning_scheme_page()->enter_password_confirmation();
$self->get_partitioning_scheme_page()->press_next();
}
Then it could be called in all the Test Modules, where the encrypted partition need to be created.
sub run {
my $partitioner = $testapi::distri->get_partitioner();
$partitioner->create_encrypted_partition();
}
But in case if need to verify some negative cases the method cannot be
used as is. For instance, to verify that prompt appears when blank
password is entered. For such kind of cases the access to the
Pages through Controller is added.
Then Test Module code may look like:
sub run {
my $partitioner = $testapi::distri->get_partitioner();
$partitioner->get_partitioning_scheme_page()->select_enable_disk_encryption_checkbox();
$partitioner->get_partitioning_scheme_page()->enter_password('');
#here assertion for prompt on blank password is performed.
}
Page layer introduces accessing methods to elements of the page/section.
It is not required to describe the accessing methods for all the elements of a screen in one class. If there is an element or a section that is common for several pages, it may be extracted into separate class and then reused by all the pages.
All the page classes (but not the page element or section classes) should be inherited from a base page (e.g. in case of pages for installation wizard, it is Installation::WizardPage).
Unlike the classic POM approach, methods of Page layer in the Framework are not returning Objects. This compromising solution was introduced because the behavior of SUT may vary depending on the steps, that were done in the previous Test Modules and also due to a large set of versions, which behavior also may differs.
Example:
package Installation::Partitioner::Libstorage::PasswordDialog;
sub press_ok {
assert_screen(ENTER_PASSWORD_DIALOG);
send_key('alt-o');
}
Do not provide any test data in Page layer. Use the test data passed from the Test Module layer instead.
This is the only layer having full access to testapi.
NOTE: Using
get_var
or similar methods to change the flow of a test should be avoided (e.g. to decide whether select checkbox or not by checking openQA variable. Please, use method parameters instead).
It should not use methods of another layers. It just provides page accessing methods for Controller layer.
Optional layer. The special Test Module that only makes sure if the actual state of the SUT corresponds to the expected one.
Useful for validating installation tests, as it is not possible to check in the installation Test Module, if the changes are applied to the system. The system need to be installed first.
- If the validation should be made via UI, consider this layer as the regular Test Module with all the appropriate conventions, like accessing to the page through Controller only.
- For console tests, there is no strict rules as of now. Use testapi directly.
-
Package and Class names should be nouns, using mixed case with the first letter of each word capitalized.
Example:
package Installation::Partitioner::Libstorage::EditProposalSettingsController;
-
Method names should be verbs, using lowercase with the underscores between the words.
Example:
sub get_password_dialog; sub edit_proposal;
-
Variable names should be lowercase with the underscores between the words.
Example:
my $is_lvm; my $filesystem;
-
Constant names should be uppercase with the underscores between the words.
Example:
use constant { SUGGESTED_PARTITIONING_PAGE => 'inst-suggested-partitioning-step', LVM_ENCRYPTED_PARTITION_IN_LIST => 'partitioning-encrypt-activated' };
-
Methods returning true/false or variables that store them, should be named beginning with is_ or has_.
Example:
sub is_lvm; sub has_separate_home; my $is_checkbox_checked; my $has_license_agreement;
Use named arguments in hash reference if Method has more than one argument.
Example:
sub edit_proposal {
($self, $args_ref) = @;
my $is_lvm = $args_ref->{is_lvm};
my $has_separate_home = $args_ref->{has_separate_home};
...
}
# Then usage:
edit_proposal({is_lvm => 1, has_separate_home => 1});
So, basically a new test requires to have at least one package/class per each layer to be created (or updated if the required class already exists).
Let's assume there might be a new test to create an account in the system during installation.
{project_root}/tests/installation/create_account.pm
use strict;
use warnings;
use parent "installbasetest";
sub run {
my $user_settings_widget = $testapi::distri->get_user_settings_widget();
$user_settings_widget->create_user({
username => 'test_user',
user_full_name => 'Test User Full Name'
});
}
1;
{project_root}/lib/Installation/UserSettingsController.pm
package Installation::UserSettingsController
use strict;
use warnings;
use parent 'Installation::WizardPage';
use Installation::UserSettingsPage;
sub new {
my ($class, $args) = @_;
my $self = bless {
user_settings_page => Installation::UserSettingsPage
}, $class;
}
sub get_user_settings_page {
my ($self) = @_;
return $self->{user_settings_page};
sub create_user {
my ($self, $args_ref) = @_;
my $username = $args_ref->{username};
my $user_full_name = $args_ref->{user_full_name};
get_user_settings_page()->fill_in_username($username);
get_user_settings_page()->fill_in_user_full_name($user_full_name);
get_user_settings_page()->fill_in_password();
get_user_settings_page()->fill_in_password_confirmation();
get_user_settings_page()->press_next();
}
1;
{project_root}/lib/Installation/UserSettingsPage.pm
package Installation::UserSettingsPage;
use strict;
use warnings;
use testapi;
use parent 'Installation::WizardPage';
use constant {
# The needle to represent the page (e.g. Title in Installation Wizard). It is used to make sure that
# action is performed on the right Page.
USER_SETTINGS_PAGE => 'user-settings-page'
};
sub fill_in_username {
my ($self, $username) = @_;
assert_screen(USER_SETTINGS_PAGE); # ensure the correct Page is shown before performing an action
send_key('alt-u'); # make the field to be in focus
type_string($username); # type the username
}
sub fill_in_user_full_name {
my ($self, $user_full_name) = @_;
assert_screen(USER_SETTINGS_PAGE); # ensure the correct Page is shown before performing an action
send_key('alt-f'); # make the field to be in focus
type_string($user_full_name); # type the User's Full Name
}
sub fill_in_password {
assert_screen(USER_SETTINGS_PAGE); # ensure the correct Page is shown before performing an action
send_key('alt-p'); # make the field to be in focus
type_password(); # testapi method to enter the default secret password
}
sub fill_in_password_confirmation {
assert_screen(USER_SETTINGS_PAGE); # ensure the correct Page is shown before performing an action
send_key('alt-o'); # make the field to be in focus
type_password(); # testapi method to enter the default secret password
}
# overrides parent 'Installation::WizardPage' method.
sub press_next {
my ($self) = @_;
$self->SUPER::press_next(USER_SETTINGS_PAGE);
}
1;
-
Let's assume all the distributions have the same implementation of the User Settings. Then add the controller to Tumbleweed distribution, as all other distributions are inherited from it to follow 'factory first'rule.
{project_root}/lib/Distribution/Opensuse/Tumbleweed.pm
package Distribution::Opensuse::Tumbleweed; use strict; use warnings FATAL => 'all'; use parent 'susedistribution'; use Installation::UserSettingsController; sub get_user_settings { return Installation::UserSettingsController->new(); } 1;
-
If some of the Distributions has different implementation of User Settings for the same feature. For example, it still allows to create new user, but with different steps.
In this case, just override the
get_user_settings
method in the required Distribution.package Distribution::Opensuse::Leap::42; use strict; use warnings FATAL => 'all'; use parent 'Distribution::Sle::12'; sub get_user_settings { return Installation::SomeAnotherImplementationOfUserSettingsController->new(); } 1;
In order to run the Test Module, it should be added to the scheduling file (e.g. main.pm)
...
loadtest 'installation/create_new_user';
...
You can also call loadtest
inside a running test to schedule additional
test modules, e.g. to install and run an external test suite in the same job.