Part 3: Expand setup code¶
In this part we are going to learn one of the key concepts of Balder - reusing tests.
This essential concept allows you to effortlessly utilize existing scenarios for various setups. Many projects require similar testing in multiple ways, whether it’s supporting a user interface on different platforms or managing a device over several apps. Perhaps you need to test a bus system where every member must function identically. Balder was designed precisely for this purpose.
There are a lot of different ways to expand our testing environment. If we have a new device, but inside the same environment, we just need to add this device to the setup. If we just want to add a new feature to an already existing device, we can just add it inside our setup. And of course, if we have a complete other environment, we can simply create a complete new setup.
In this section, we’ll add a new setup class.
How the all-in-one setup approach works is described in Part 5: One common setup.
So let’s go back to our example and restructure our earlier created project a little bit.
Prepare for the new setup¶
In part 2 we have already implemented a setup for our login server, that uses the web frontend to login to our internal page. Often we also need an API/REST interface to connect with internal components. Our loginserver also supports this. We want to add a new setup for this method now.
First of all, think about the features we need for it.
Think about the devices¶
Similar to part 2 we could think about the required features we need for our new setup class. If we go back to the scenario definition we need the following features:
Loginserver
:
HasLoginSystemFeature
(autonomous feature)ValidRegisteredUserFeature
RestClient
InsertCredentialsFeature
ViewInternalPageFeature
As you can see we need the same features like we have used earlier in our first setup. Of course we need the same one, because we still have the same procedure. Similar to the webpage login, we need the procedure defined in part 1:
check that we have no access to the internal page/data
insert a valid username
insert a valid password
press submit/send request
check that we have access to the internal page/data
logout (over request)
check that we have no access to the internal page/data
The procedure is the same, we only must do it a little bit different. With that we don’t have to change the scenario and also we can still use some similar setup features. So with the first step, we want to refactor our setup directory to reuse some setup feature classes.
Refactor our setup directory¶
We want to sort our setup classes into individual directories. We create a new directory setups/browser
and move
the setup_web_browser.py
file into it. In addition to that we rename our setups/features.py
file to
setup_features.py
. For this changes, we have to update our import statements too. Our new refactored directory
should now looks like the following:
- balderexample-loginserver/
|- ...
|- tests
...
|- setups
|- browser
|- __init__.py
|- setup_web_browser.py
|- __init__.py
|- setup_features.py
Now we want to organize our features of the old setup. All features usable without a browser should be placed
in the setup_features.py
file. All specific features that are implemented to test the login with the
browser should be placed in a separate feature file setups/browser/browser_features.py
.
So our new structure looks like the following:
- balderexample-loginserver/
|- ...
|- tests
...
|- setups
|- browser
|- __init__.py
|- browser_features.py
|- setup_web_browser.py
|- __init__.py
|- setup_features.py
Note
As you can see, it could be helpful to organize your feature inside own namespace modules. Of course you can
organize your project in the structure of your choice. You can also name it in the way you want,
but it is highly recommended to use a name to easily distinguish the source of an imported setup. If you name every
file features.py
this can get really hard to understand, specially when you import from different directory
levels, like it is showed below.
from .features import X, Y
from ..features import P, Z
...
Its easier if you rename the files, like we have done above:
from . import browser_features
from .. import setup_features
...
class SetupExample(balder.Setup):
class Browser(balder.Device):
glob = setup_features.GlobFeature()
browser = browser_features.SpecialBrowserFeature()
...
So think about which features are global and which features are special browser features. If you take a look into
our file setup_features.py
you should find the following features:
MyValidRegisteredUserFeature
: This feature provides the user credentials valid for the wholebalderexample-loginserver
. The user is valid for all access strategies.LoginWebpageFeature
: This feature provides all specific data of the login front-end webpage.InternalWebpageFeature
: This feature provides all specific data of the internal front-end webpage.MyInsertCredentialsFeature
: This feature allows inserting credentials into a login system.MyViewInternalPageFeature
: This feature allows the owner device to interact with the internal area provided by the vDevice.
The first feature MyValidRegisteredUserFeature
returns the global valid credentials to access the login area in
every possible way. This feature is not limited to the browser method, so we can left it in the higher file
setups/setup_features.py
. All the other features, however, are specific, so we move them to the browser specific
file setups/browser/setup_web_browser.py
.
Implement the REST setup¶
Ok so we have redesigned our environment now. It is time to add a new setup. The balderexample-loginserver
package
also provides a REST api, that allows us to request all existing users, but of course only if we are logged in.
We want to create a setup that allows us to request all registered users. For this we can ask the
endpoint /api/users
. But this endpoint contains sensitive data, so it is behind an authentication system. Our
server uses a standard authentication system Basic Authentication
that requires the same username and password as
credentials, we also have used in the browser setup before. We can use the python library requests
, which
allows us easily to execute a GET request with Basic Authentication
.
Install requests¶
For testing our API, we use the python package requests
. Make sure that you have installed it.
>>> pip install requests
Add the new file¶
First of all, we want to create the new file. We are adhering to our new structure and add a new module in our
setup directory first. We can name it setups/rest
. There we add a new file rest_features.py
for our rest
specific features and a new setup_rest_basicauth.py
, which will contain our setup implementation. Our directory
should look like the following:
- balderexample-loginserver/
|- ...
|- tests
...
|- setups
|- browser
|- __init__.py
|- browser_features.py
|- setup_web_browser.py
|- rest
|- __init__.py
|- rest_features.py
|- setup_rest_basicauth.py
|- __init__.py
|- setup_features.py
Similar to part 2 we first define our new setup with the devices and all imported features. Again we want to create two devices, one server devices that provides the rest api and one rest client device, that executes the requests with the basic authentication.
Similar to the first setup, we name the features in a format My<scenario feature name>
. We will import them all from
our new specific rest file setups/rest/rest_features.py
, except for our MyValidRegisteredUserFeature
, which we
have already moved in the common setup-feature file setups/setup_features.py
.
Our setup file will look like:
import balder
from balder.connections import HttpConnection
from tests.lib.features import HasLoginSystemFeature
from tests.setups import setup_features
from tests.setups.rest import rest_features
class SetupRestBasicAuth(balder.Setup):
class Server(balder.Device):
# the autonomous feature can be imported directly
_ = HasLoginSystemFeature()
# we have imported it from our common setup-feature file
valid_user = setup_features.MyValidRegisteredUserFeature()
@balder.connect(Server, HttpConnection)
class Client(balder.Device):
# all of the following files are rest specific files - these are imported from the specific feature file
credentials = rest_features.MyInsertCredentialsFeature()
internal = rest_features.MyViewInternalPageFeature()
Add the REST specific features¶
We have added two features that requires a own REST specific implementation. Let us add these features in the file:
import balder
from ...lib.features import InsertCredentialsFeature, ViewInternalPageFeature
# Client features
class MyInsertCredentialsFeature(InsertCredentialsFeature):
class Server(InsertCredentialsFeature.Server):
pass
def insert_username(self, username):
pass
def insert_password(self, password):
pass
def execute_login(self):
pass
def execute_logout(self):
pass
class MyViewInternalPageFeature(ViewInternalPageFeature):
class Server(ViewInternalPageFeature.Server):
pass
def check_internal_page_viewable(self):
pass
As you can see, we have also overwritten the vDevice instances, because we will need them in this features too. Similar to the part 2 we need a common feature that provides access to our api endpoint. Even though we don’t really have a login area here, but actually send the access data with each request, we want to set up the whole thing similarly.
Add the basic auth manager to our REST features¶
We want to use this file as required feature in our specific rest features. As you know, this can be done by simply instantiating it inside the specific rest features that need it:
import balder
from ...lib.features import InsertCredentialsFeature, ViewInternalPageFeature
# Client features
class MyInsertCredentialsFeature(InsertCredentialsFeature):
class Server(InsertCredentialsFeature.Server):
pass
basic_auth_manager = BasicAuthManager()
username = None
password = None
def insert_username(self, username):
self.username = username
def insert_password(self, password):
self.password = password
def execute_login(self):
self.basic_auth_manager.set_credentials(self.username, self.password)
return True
def execute_logout(self):
self.basic_auth_manager.reset_credentials()
return True
class MyViewInternalPageFeature(ViewInternalPageFeature):
class Server(ViewInternalPageFeature.Server):
pass
basic_auth_manager = BasicAuthManager()
def check_internal_page_viewable(self):
response = self.basic_auth_manager.request_webpage("TODO add the endpoint")
return response.status_code == 200
Nice, we already have the main implementation. The only thing, we still need is the endpoint url of the server.
Add the server feature¶
For this we have to add a feature to the server vDevice, that provides these information. Let’s call this the
UserApiFeature
. It should only contain a property which returns the url here. In order for us to use it, we only
have to instantiate it in our vDevice class:
import balder
from ...lib.features import InsertCredentialsFeature, ViewInternalPageFeature
# Server features
class UserApiFeature(balder.Feature):
@property
def url_users(self):
return "http://localhost:8000/api/users"
# Client features
class MyInsertCredentialsFeature(InsertCredentialsFeature):
class Server(InsertCredentialsFeature.Server):
pass
basic_auth_manager = BasicAuthManager()
username = None
password = None
def insert_username(self, username):
self.username = username
def insert_password(self, password):
self.password = password
def execute_login(self):
self.basic_auth_manager.set_credentials(self.username, self.password)
return True
def execute_logout(self):
self.basic_auth_manager.reset_credentials()
return True
class MyViewInternalPageFeature(ViewInternalPageFeature):
class Server(ViewInternalPageFeature.Server):
api = UserApiFeature()
basic_auth_manager = BasicAuthManager()
def check_internal_page_viewable(self):
response = self.basic_auth_manager.request_webpage(self.Server.api.url_users)
return response.status_code == 200
Of course we have to add our new helper features in our REST setup too:
import balder
from balder.connections import HttpConnection
from tests.lib.features import HasLoginSystemFeature
from tests.setups import setup_features
from tests.setups.rest import rest_features
class SetupRestBasicAuth(balder.Setup):
class Server(balder.Device):
_ = HasLoginSystemFeature()
# our new helper feature
api_route = rest_features.UserApiFeature()
valid_user = setup_features.MyValidRegisteredUserFeature()
@balder.connect(Server, HttpConnection)
class Client(balder.Device):
# our new helper feature
basicauth_manager = rest_features.BasicAuthManager()
credentials = rest_features.MyInsertCredentialsFeature()
internal = rest_features.MyViewInternalPageFeature()
We have made it! We have implemented both setups and manage the common use of feature classes. So let’s start Balder.
Execute Balder with both setups¶
We can check if Balder resolves our scenario with the both setups correctly. For this, just call Balder with the
argument --resolve-only
:
$ balder --working-dir tests --resolve-only
+----------------------------------------------------------------------------------------------------------------------+
| BALDER Testsystem |
| python version 3.9.7 (default, Sep 3 2021, 12:37:55) [Clang 12.0.5 (clang-1205.0.22.9)] | balder version 0.0.1 |
+----------------------------------------------------------------------------------------------------------------------+
Collect 2 Setups and 1 Scenarios
resolve them to 2 mapping candidates
RESOLVING OVERVIEW
Scenario `ScenarioSimpleLoginOut` <-> Setup `SetupWebBrowser`
ScenarioSimpleLoginOut.ClientDevice = SetupWebBrowser.Client
ScenarioSimpleLoginOut.ServerDevice = SetupWebBrowser.Server
-> Testcase<ScenarioSimpleLoginOut.test_valid_login_logout>
Scenario `ScenarioSimpleLoginOut` <-> Setup `SetupRestBasicAuth`
ScenarioSimpleLoginOut.ClientDevice = SetupRestBasicAuth.Client
ScenarioSimpleLoginOut.ServerDevice = SetupRestBasicAuth.Server
-> Testcase<ScenarioSimpleLoginOut.test_valid_login_logout>
Great, it works. Balder can find our two possible variations, one using our SetupWebBrowser
and one using our
SetupRestBasicAuth
.
Now it is time to really run the Balder session.
Note
Do not forget to start the django server before:
$ python manage.py runserver
After you have secured that the django server runs, you can run Balder:
$ balder --working-dir tests
+----------------------------------------------------------------------------------------------------------------------+
| BALDER Testsystem |
| python version 3.9.5 (default, Nov 23 2021, 15:27:38) [GCC 9.3.0] | balder version 0.0.1 |
+----------------------------------------------------------------------------------------------------------------------+
Collect 2 Setups and 1 Scenarios
resolve them to 2 mapping candidates
================================================== START TESTSESSION ===================================================
SETUP SetupRestBasicAuth
SCENARIO ScenarioSimpleLoginOut
VARIATION ScenarioSimpleLoginOut.ClientDevice:SetupRestBasicAuth.Client | ScenarioSimpleLoginOut.ServerDevice:SetupRestBasicAuth.Server
TEST ScenarioSimpleLoginOut.test_valid_login_logout [✓]
SETUP SetupWebBrowser
SCENARIO ScenarioSimpleLoginOut
VARIATION ScenarioSimpleLoginOut.ClientDevice:SetupWebBrowser.Client | ScenarioSimpleLoginOut.ServerDevice:SetupWebBrowser.Server
TEST ScenarioSimpleLoginOut.test_valid_login_logout [✓]
================================================== FINISH TESTSESSION ==================================================
TOTAL NOT_RUN: 0 | TOTAL FAILURE: 0 | TOTAL ERROR: 0 | TOTAL SUCCESS: 2 | TOTAL SKIP: 0 | TOTAL COVERED_BY: 0