diff --git a/doc/source/_static/nextcloud_login_screen.png b/doc/source/_static/nextcloud_login_screen.png new file mode 100644 index 0000000..8cb4e6a Binary files /dev/null and b/doc/source/_static/nextcloud_login_screen.png differ diff --git a/doc/source/tutorial_guide/00_intro.rst b/doc/source/tutorial_guide/00_intro.rst index 1ba37ab..63f7161 100644 --- a/doc/source/tutorial_guide/00_intro.rst +++ b/doc/source/tutorial_guide/00_intro.rst @@ -1,6 +1,269 @@ Balder Intro Example ******************** +In this example, we will explore the key aspects of the Balder system. We start by writing a full test from scratch, +then continue by adding tests that were already written for us through installation. Finally, we demonstrate Balder's +flexibility by porting our previously developed web tests to use a command-line interface and a Unix filesystem. + +Instead of inventing a fictional example, we will test a real application. For this purpose, we will use +`Nextcloud `_. + +What is Nextcloud? +================== + +Nextcloud is an open-source software platform designed for self-hosted file storage and collaboration. It allows users +to set up their own cloud server, similar to commercial services like Dropbox or Google Drive, but with a strong +emphasis on user control and data privacy. Individuals or organizations can manage files, share them securely, and +decide who has access - without relying on external companies. (We will use installable tests for that in +`Part 2: Install Tests for NextCloud Web `_ and +`Part 3: Reuse Web Tests for NextCloud CLI `_.) + +The platform offers many more tools than just syncing files across devices. For example, it supports editing documents +in real time, managing calendars and contacts, and handling video calls. However, in this tutorial, we will limit our +testing to the file syncing module. + +Preparations +============ + +Our tests will begin by testing the Nextcloud web interface. To do this, we will create a local instance of Nextcloud +using Docker. + +What is Docker? +--------------- + +Docker is an open-source platform that simplifies the process of building, shipping, and running applications. It does +this by using lightweight, isolated environments called containers. These containers package your software along with +all its dependencies in a standardized way. This approach is similar to virtual machines but much more efficient and +portable, as it doesn't require a full operating system for each instance. + +Fortunately, Nextcloud offers a `ready-to-use docker container `_ that we can +easily set up for our tests. + +Install Docker +-------------- + +If you haven't installed docker yet, please have a look at the official documentation. + +To set up Docker on your system, you'll need to install the Docker Engine, which is the core component for running +containers. Additionally, make sure to include `Docker Compose `_, a tool that helps +you define and manage multi-container applications using simple configuration files. This setup is straightforward, but +it varies slightly depending on your operating system. We'll focus on Unix-based systems (like Linux) and Windows here, +with links to the +`official Docker documentation for step-by-step guidance `_. + +**Linux User:** + +For Linux users, installing the Docker Engine is usually sufficient, as Docker +Compose is now integrated as a plugin in recent versions. Start by following the official installation guide for your +specific Linux distro, which covers adding the Docker repository, installing the engine, and verifying it works. You can +find the detailed steps at the official Docker Engine installation page: https://docs.docker.com/engine/install/. + +.. note:: + Additionally, ensure that you install the Docker Compose plugin (often available as the package + ``docker-compose-plugin``). If it's not already installed, you may need to do so manually. For detailed + instructions and any additional commands, check the official Docker Compose documentation at + https://docs.docker.com/compose/install/. + +**Mac Users** + +For Mac users, setting up Docker is simple using Docker Desktop, which includes the Docker Engine for running +containers and Docker Compose for managing multi-container setups. Download and install Docker Desktop from the +official site, then follow the guided installation process, which handles everything including permissions and +integration with macOS. The official guide provides detailed steps, including system requirements like macOS version +and hardware: https://docs.docker.com/desktop/install/mac-install/. + +**Windows User** + +If you're on Windows, you'll need to use the **Windows Subsystem for Linux (WSL)** as the backend, since we are using +a Linux container for Nextcloud. First, enable WSL through your Windows settings or PowerShell, then install Docker +Desktop, which bundles the engine, Compose, and other tools. + +The official guide walks you through this process clearly, including WSL setup. + +Visit the Docker Desktop for Windows installation page at https://docs.docker.com/desktop/install/windows-install/ for +the full instructions. + +Once installed, you can test everything by running a simple container from the command line. + +Create a Docker Compose File +---------------------------- + +To make our lives easier, we will set up a Docker Compose file. These files simplify managing containers, especially +since we are going to extend this setup later in the tutorial. + +Let's get started. First, create a new project directory. Then, copy the following content into a file named +``docker-compose.yml`` within that directory: + +.. code-block:: yaml + + services: + nextcloud: + # project https://github.com/nextcloud/docker + image: nextcloud:stable + environment: + - SQLITE_DATABASE=nextcloudsqlite + - NEXTCLOUD_TRUSTED_DOMAINS=nextcloud + - NEXTCLOUD_ADMIN_USER=admin + - NEXTCLOUD_ADMIN_PASSWORD=Admin12345 + ports: + - 127.0.0.1:8000:80 + +We have made some definitions here. We want to use the local SQLite database, which is sufficient for our testing demo. +We also want an admin user with the username ``admin`` and password ``Admin12345``. Additionally, we want to bind the +container's port 80 to our localhost port 8000. This is necessary so that we can access the web page from our host +machine. + +You can start your Compose environment by calling: + +.. code-block:: shell + + $ docker compose up -d + +This command will start the containers defined in the Docker Compose file. You should see output similar to the +following: + +.. code-block:: none + + [+] Running 2/2 + ✔ Network project_default Created + ✔ Container project-nextcloud-1 Started .. note:: - This tutorial is coming soon! It will be released on 25th of October. \ No newline at end of file + Don't forget to shut down your container when you are finished, otherwise the container will continue running. You + can do this with + + .. code-block:: shell + + $ docker compose down + +Verify NextCloud is running +--------------------------- + +As soon as we start the containers with docker compose, the nextcloud application should start. You can verify that +by opening a website with http://localhost:8000. + +You should be able to see the login screen: + +Once you start the containers using Docker Compose, the Nextcloud application should be up and running. To verify this, +open your web browser and go to http://localhost:8000. + +You should see the login screen: + +.. image:: /_static/nextcloud_login_screen.png + +Set Up Selenium +--------------- + +We want to test the web app using Selenium as the GUI control engine. To do this, make sure you have a working +WebDriver that Selenium can use to control the browser. + +If you prefer to use your local browser, check the official documentation for instructions on setting up your system. + +Alternatively, you can use Selenium Grid, which runs in its own Docker container. This lets you test in an isolated +environment. If you want to go this route, add the following service to your ``docker-compose.yml`` file: + +.. code-block:: yaml + + services: + nextcloud: + ... + firefox: + image: selenium/standalone-firefox:4.35 + shm_size: 2gb + ports: + - "127.0.0.1:4444:4444" + +This configuration allows you to establish a remote connection with Selenium. You can access the web frontend at +http://localhost:4444/, which lets you view active sessions and connect to them. + +Restart your Docker Compose environment to start this container too: + +.. code-block:: + + $ docker compose down && docker compose up -d + +When you open a web browser and navigate to http://localhost:4444, you should see the Selenium Grid interface. Once a +Selenium session becomes active, you can use this interface to connect to the container and observe what the test is +doing. + +Set up Python +============= + +Now that we have prepared the environment, the final step is to set up Python and install all the dependencies needed +to run tests with Balder. To do this, let's create a virtual environment. + +Set up virtual environment +-------------------------- + +Virtual environments allow you to separate your system's installed packages from your project's dependencies. This way, +you can install project-specific packages only at the project level, without affecting your system packages. For more +information about virtual environments, check out +`the official venv documentation `_. + +You can create a new virtual environment simply by running the built-in command. Navigate to the root directory of +your new project (where you stored the docker-compose.yml file) and execute the following command: + +.. code-block:: + + >>> python3 -m venv .venv + +This command will create the virtual environment. + +After creating the virtual environment, you need to activate it. The activation command depends on your operating +system: + +.. list-table:: Activate VirtualEnv + :widths: 25 25 50 + :header-rows: 1 + + * - OS + - Shell + - Command + * - POSIX + - bash/zsh + - $ source venv/bin/activate + * - POSIX + - fish + - $ source venv/bin/activate.fish + * - POSIX + - csh/tcsh + - $ source venv/bin/activate.csh + * - POSIX + - PowerShell Core + - $ source venv/bin/Activate.ps1 + * - POSIX + - csh/tcsh + - $ source venv/bin/activate.csh + * - POSIX + - PowerShell Core + - $ venv/bin/Activate.ps1 + * - WINDOWS + - cmd.exe + - C:\> venv\Scripts\activate.bat + * - WINDOWS + - PowerShell + - PS C:\> venv\Scripts\Activate.ps1 + +.. note:: + You must activate the virtual environment (using the appropriate command from above) in every new terminal + session before you can use it. + +Install requirements +-------------------- + +Before we begin, make sure to install Balder in your new virtual environment: + +.. code-block:: + + $ pip install baldertest + +Since we are using Selenium for browser automation in this tutorial, you should also install the ``balderhub-selenium`` +package. This package provides features you can directly use later on. Run the following command too: + +.. code-block:: + + $ pip install balderhub-selenium + +Now that we are all set up, we can begin the hands-on part of the tutorial. In this first section, we will focus on +testing the login mechanism of Nextcloud. Let's dive right in with +`Part 1: Develop a Login Test from Scratch `_. diff --git a/doc/source/tutorial_guide/01_develop_first_test.rst b/doc/source/tutorial_guide/01_develop_first_test.rst index c9ab841..e1e09ee 100644 --- a/doc/source/tutorial_guide/01_develop_first_test.rst +++ b/doc/source/tutorial_guide/01_develop_first_test.rst @@ -1,6 +1,491 @@ Part 1: Develop a Login Test from Scratch ***************************************** +As described in the `Balder Intro Example `_, we want to test the official Nextcloud app. For this, we set up Docker to +be able to test the application locally. .. note:: - This tutorial is coming soon! It will be released on 25th of October. \ No newline at end of file + Make sure you have completed the preparation steps and installed Docker, Docker Compose, and Balder. You can find + an explanation about that in `Balder Intro Example `_. + +In this first part of this tutorial, we'll test the login process of the Nextcloud app. Normally, we would use the +``ScenarioSimpleLogin`` from the ``balderhub-auth`` package, since the test is already implemented there. However, for +this tutorial, we'll implement the test from scratch to help you understand the process better. + +Prepare Environment +=================== + +Before we start, we need to make sure that our test environment is ready. + +Start Docker container +---------------------- + +Let's verify that the docker containers are running: + +.. code-block:: shell + + $ docker compose up -d + +Open the browser and go to http://localhost:8000. You should see the NextCloud login page, we want to test now. + +Create initial filestructure +---------------------------- + +Let's start by creating some files in an easy-to-use file structure: + +.. code-block:: none + + - + | - src + | - lib + | - scenario_features.py + | - setup_features.py + | - scenarios/ + | - scenario_login.py + | - setups/ + | - setup_docker.py + +This design adheres to the design that is frequently used in Balderhub projects and also by the +`template generator for BalderHub projects `_. + +Install ``balderhub-html`` +-------------------------- + +We want to develop a test from scratch, but we want to develop a web test. Our live will get much simpler, when we are +using the package ``balderhub-html``. This project provides html components, we can directly use in our tests, which +provides different methods to make sure that it reduces flaky tests, which is often the case for asynchron web apps. + +We want to develop a test from scratch, specifically a web test. Our lives will become much simpler if we use the +package ``balderhub-html`` for that. This package provides HTML components that we can directly use in our tests. These +components offer various methods to reduce flaky tests, which are common in web apps. + +.. note:: + When using ``balderhub-html``, you can write web tests without worrying about how to control them. This package + requires a control feature that implements the ``balderhub-guicontrol`` interface, but as an end user, this detail + is completely irrelevant. Simply install the GUI control package of your choice (we're using ``balderhub-selenium`` + in this tutorial, but you can also use ``balderhub-appium`` (coming soon), ``balderhub-playwright`` (coming soon), + or any other package that supports the ``balderhub-guicontrol`` interface). + + +You can install this package with: + +.. code-block:: shell + + $ pip install balderhub-html + +Developing the Test Scenario +============================ + +So let's start by developing a test scenario. For this, we need to define **what is needed** to execute the contained +test. + +We want to write a test that opens the login page, enters a username and password, and presses the login button. After +that, we also want to check if the user is really logged in. + +So let's start defining such a scenario. Often, it helps to just write down what you want to have. In Balder, everything +is organized around devices, and these devices have features. For example, we could have a ``LoginFeature`` feature, and +we can think about having two different devices: a Browser and a WebServer. So let's start with that: + +.. code-block:: python + + # file `scenarios/scenario_login.py` + + import balder + + from lib.scenario_features import LoginFeature + + class ScenarioLogin(balder.Scenario): + + class WebServer(balder.Device): + pass + + @balder.connect('WebServer', over_connection=balder.Connection()) + class Browser(balder.Device): + login = LoginFeature() + + def test_login(self): + username = "admin" # TODO + password = "Admin12345" # TODO + + assert not self.Browser.login.user_is_logged_in(), "some user is already logged in" + + self.Browser.login.type_username(username) + self.Browser.login.type_password(password) + self.Browser.login.submit_login() + + assert self.Browser.login.user_is_logged_in(), "user was not logged in" + +Note that we've set hardcoded values for the username and password for now. These will be replaced later, as we're going +to develop a scenario that can be used for all kinds of logins - not just our specific case with NextCloud and with the +specific username and password. For the time being, we'll leave them as they are. + +We've added an import for our future feature ``LoginFeature``, which isn't implemented yet. Since we're working on the +scenario, this feature should be placed in ``lib/scenario_features.py``. Now, let's define it. Before doing that, take +a look at our test method itself: Which methods do we need in our future feature? + +We are using the methods ``user_is_logged_in()``, ``type_username()``, ``type_password()``, and ``submit_login()``. +Okay, that's it - these will be the methods for our future feature ``LoginFeature``. Let's define it now. + +Define our Scenario-Level-Feature +--------------------------------- + +On the scenario level, we define **what is needed** without necessarily providing an exact implementation of how it is +realized. + +With that in mind, we'll define this feature using abstract methods only: + +.. code-block:: python + + # file `lib/scenario_features.py` + + import balder + + class LoginFeature(balder.Feature): + + def user_is_logged_in(self): + raise NotImplementedError + + def type_username(self, username: str): + raise NotImplementedError + + def type_password(self, password: str): + raise NotImplementedError + + def submit_login(self): + raise NotImplementedError + + +That's it. Everything on the scenario level is now defined. + +That concludes the first part. We've created a login test that can be reused for various purposes. It doesn't matter +whether you want to test the login of a website (as we're doing here) or something entirely different, like the login on +an electric door gate, for example. + +Provide the implementation with a Setup +======================================= + +When we run Balder later, it will try to find matches between the scenario and our defined setup classes. To do this, +Balder checks if there is at least one device in the setup that provides an implementation for every feature in our +scenario device. An implementation is provided by a feature that is a subclass of the corresponding scenario feature. + +If we use more than one feature in the scenario class, Balder will also check for other devices that fulfill the same +feature implementation conditions. Additionally, it validates that these devices are connected using the exact +connections specified. + +You can read more about the mechanism of how Balder works in `this guide `__. For details on +how connections can be used to select specific variations, see `this guide `__. + +Define the Setup +---------------- + +But for now, let's start by defining a setup that can be used for our specific case: + +.. code-block:: python + + import balder + from lib.setup_features import LoginFeature + + class SetupDocker(balder.Setup): + + class NextCloud(balder.Device): + pass + + @balder.connect("NextCloud", over_connection=balder.Connection()) + class SeleniumBrowser(balder.Device): + login_func = LoginFeature() + + +We directly imported a non-existent feature called ``LoginFeature`` from ``lib.setup_features``. This feature doesn't +exist yet, but we'll define it shortly to provide the implementation for our scenario feature +``lib.scenario_features.LoginFeature``. + +Define the Setup-Based ``LoginFeature`` +--------------------------------------- + +Now, let's define this feature by creating a new class in ``lib/setup_features.py``. This class should inherit directly +from ``lib.scenario_features.LoginFeature`` and provide implementations for all the abstract methods: + +.. code-block:: python + + # file `lib/setup_features.py` + + import balder + import lib.scenario_features + + class LoginFeature(lib.scenario_features.LoginFeature): + + def user_is_logged_in(self): + # todo provide an implementation + pass + + def type_username(self, username: str): + # todo provide an implementation + pass + + def type_password(self, password: str): + # todo provide an implementation + pass + + def submit_login(self): + # todo provide an implementation + pass + +Here, we'll add our implementation soon. But for now, this is enough to run Balder: + +.. code-block:: shell + + $ balder --working-dir src + +.. code-block:: none + + +----------------------------------------------------------------------------------------------------------------------+ + | BALDER Testsystem | + | python version 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] | balder version 0.1.0b14 | + +----------------------------------------------------------------------------------------------------------------------+ + Collect 1 Setups and 1 Scenarios + resolve them to 1 valid variations + + ================================================== START TESTSESSION =================================================== + SETUP SetupDocker + SCENARIO ScenarioLogin + VARIATION ScenarioLogin.Browser:SetupDocker.SeleniumBrowser | ScenarioLogin.WebServer:SetupDocker.NextCloud + TEST ScenarioLogin.test_login [X] + ================================================== FINISH TESTSESSION ================================================== + TOTAL NOT_RUN: 0 | TOTAL FAILURE: 1 | TOTAL ERROR: 0 | TOTAL SUCCESS: 0 | TOTAL SKIP: 0 | TOTAL COVERED_BY: 0 + + Traceback (most recent call last): + File "/home/user/temp_balder_tutorial/.venv/lib/python3.12/site-packages/_balder/executor/testcase_executor.py", line 132, in _body_execution + self.base_testcase_callable(self=self.base_testcase_obj, **all_args) + File "/home/user/temp_balder_tutorial/src/scenarios/scenario_login.py", line 25, in test_login + assert self.Browser.login.user_is_logged_in(), "user was not logged in" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + AssertionError: user was not logged in + +Of course, we get an error, because we haven't provided any implementation and the test does not really do something (the +methods are still empty), but everything gets collected and Balder can find the match. + +Of course, we get an error because we haven't provided any implementation yet, and the test doesn't really do anything +(the methods are still empty). However, everything gets collected, and Balder can find the match. + +Provide an Implementation for the Scenario-Based ``LoginFeature`` +----------------------------------------------------------------- + +When we want to provide an implementation for the ``lib.setup_features.LoginFeature``, we can either write it from +scratch - importing Selenium, setting it up, handling waiting functions, and so on - or we can simply use +``balderhub-html`` and ``balderhub-selenium``. + +So let's make sure, that we have installed them: + +.. code-block:: shell + + $ pip install balderhub-html balderhub-selenium + +Before we add the html elements, let's add the selenium feature. We need a feature that supports the +guicontrol interface (see `balderhub-guicontrol `_). The +balderhub-guicontrol packages handle all the required management for you, so you do not need to do something, except +using one of the features that implements its interfaces. + +Before we add the HTML elements, let's incorporate the Selenium feature. We need a feature that supports the guicontrol +interface (see `balderhub-guicontrol `_). The ``balderhub-guicontrol`` +packages handle all the required management for you, so you don't need to do anything special - except to use one of +the features that implements this interface. + +.. code-block:: python + + # file `lib/setup_features.py` + + import balder + import lib.scenario_features + from balderhub.selenium.lib.scenario_features import SeleniumFeature + + class LoginFeature(lib.scenario_features.LoginFeature): + + selenium = SeleniumFeature() + + ... + +As you can see, we've added a ``SeleniumFeature`` from ``balderhub-selenium``. But wait - this is a feature within a +feature. What does that mean? It means Balder will ensure that whenever this feature is used, the instantiating device +must also have a ``SeleniumFeature`` (or a subclass of it). Balder will then automatically assign that instance to the +selenium class attribute of our ``LoginFeature``. You don't need to do anything special for this; Balder handles it all +behind the scenes. However, you can certainly make use of it in your code. + +So, let's take advantage of it and provide the implementation: + + +.. code-block:: python + + # file `lib/setup_features.py` + + import balder + import lib.scenario_features + from balderhub.html.lib.utils import Selector + import balderhub.html.lib.utils.components as html + from balderhub.selenium.lib.scenario_feature import SeleniumFeature + + class LoginFeature(lib.scenario_features.LoginFeature): + + selenium = SeleniumFeature() + + # the url to navigate to be able to login + login_url = "http://nextcloud/login" + + @property + def input_username(self): + # html element where we can type in the username + return html.inputs.HtmlTextInput.by_selector(self.selenium.driver, Selector.by_name('user')) + + @property + def input_password(self): + # html element where we can type in the password + return html.inputs.HtmlPasswordInput.by_selector(self.selenium.driver, Selector.by_name('password')) + + @property + def btn_login(self): + # html