Skip to content

Commit

Permalink
Make async feature (#8)
Browse files Browse the repository at this point in the history
* - Add asynchronous modules and classes
- Add sync version of the async test for performance comparison
- Change asynchronous tests to run asynchronously
- Unit test passing

* Fix issues

* fix issue

* remove 'Execution with Caqui' session from README

* small fixes

* reorder logs in Application

* Improve report error of async OppenApp and CloseApp
  • Loading branch information
douglasdcm authored Jan 5, 2025
1 parent f3e8555 commit 72a18d0
Show file tree
Hide file tree
Showing 16 changed files with 399 additions and 119 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ python -m pytest

## Linter
```
flake8 --exclude venv --max-line-length 100
flake8 --exclude venv,.tox --max-line-length 100
```

# Tox
Expand Down
16 changes: 1 addition & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ This framework can be installed by
pip install guara
```

# Execution with Selenium
# Execution
It is recommended to use `pytest`

```
Expand Down Expand Up @@ -147,20 +147,6 @@ INFO guara.transaction:transaction.py:37 ---
PASSED
```

# Execution with [Caqui](https://github.com/douglasdcm/caqui) (async)
```
# runs the example TestAsyncTransaction.
# Go to test_async.py to check requisites to start Caqui
python -m pytest -n auto -k TestAsyncTransaction -s
```
Outputs
```
4 workers [2 items]
..
============================================================= 2 passed in 3.56s ==============================================================
```

# Tutorial
Read the [step-by-step](https://github.com/douglasdcm/guara/blob/main/TUTORIAL.md) to build your first automation with this framework.

Expand Down
8 changes: 4 additions & 4 deletions TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test_canonical():
google.at(setup.CloseApp)
```

# Basic pratical example
# Basic practical example

This is a basic search of the term "guara" on Google. To follow the steps create the files `home.py` and `test_tutorial.py` at the same folder.

Expand Down Expand Up @@ -59,7 +59,7 @@ import home

from selenium import webdriver

# Imports the Application to buld and run the automation
# Imports the Application to build and run the automation
from guara.transaction import Application

# Imports the module with the strategies to asset the result
Expand Down Expand Up @@ -99,7 +99,7 @@ def test_google_search(setup_app):
google = setup_app

# With the `app` received from the fixture the similar things
# explaned previouslly in the fixture happens.
# explained previouslly in the fixture happens.
# The transaction `home.Search` with the parameter `text`
# is passed to `at` and the result is asserted by `asserts` with
# the strategy `it.IsEqualTo`
Expand All @@ -108,7 +108,7 @@ def test_google_search(setup_app):
```
- `class Application`: is the runner of the automation. It receives the `driver` and passes it hand by hand to transactions.
- `def at`: receives the transaction created on `home.py` and its parameters. Notice the usage of the module name `home` to make the readability of the statement as plain English. The parameters are passed explictly for the same purpose. So the `google.at(home.Search, text="guara")` is read `Google at home [page] search [for] text "guara"`. The terms `page` and `for` could be added to the implementation to make it more explict, like `google.at(homePage.Search, for_text="guara")`. This is a decision the tester may make while developing the transactions.
- `def asserts`: receives a strategy to compare the result against an expected value. Again, the focous on readability is kept. So, `asserts(it.IsEqualTo, "All")` can be read `asserts it is equal to 'All'`
- `def asserts`: receives a strategy to compare the result against an expected value. Again, the focus on readability is kept. So, `asserts(it.IsEqualTo, "All")` can be read `asserts it is equal to 'All'`
- `it.IsEqualTo`: is one of the strategies to compare the actual and the expected result. Other example is the `it.Contains` which checks if the value is present in the page. Notice that the assertion is very simple: it validates just one value. The intention here is keep the framework simple, but robust. The tester is able to extend the strategies inheriting from `IAssertion`.

# Extending assertions
Expand Down
Empty file added guara/asynchronous/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions guara/asynchronous/it.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Any, TYPE_CHECKING

if TYPE_CHECKING:
from guara.asynchronous.transaction import AbstractTransaction


class IAssertion:
async def asserts(self, actual: "AbstractTransaction", expected: Any) -> None:
raise NotImplementedError


class IsEqualTo(IAssertion):
async def asserts(self, actual, expected):
assert actual.result == expected


class IsNotEqualTo(IAssertion):
async def asserts(self, actual, expected):
assert actual.result != expected


class Contains(IAssertion):
async def asserts(self, actual, expected):
assert expected.result in actual


class DoesNotContain(IAssertion):
async def asserts(self, actual, expected):
assert expected.result not in actual
37 changes: 37 additions & 0 deletions guara/asynchronous/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from guara.asynchronous.transaction import AbstractTransaction


class OpenApp(AbstractTransaction):
"""
Not Implemented as Selenium not is executed asynchronously.
Use your preferable asynchronous Web Driver.
For example: https://github.com/douglasdcm/caqui
"""

def __init__(self, driver):
super().__init__(driver)

async def do(self, **kwargs):
raise NotImplementedError(
"Selenium does not support asynchronous execution."
" Use your preferable async WebDriver. "
" For example https://github.com/douglasdcm/caqui"
)


class CloseApp(AbstractTransaction):
"""
Not Implemented as Selenium is not executed asynchronously.
Use your preferable asynchronous Web Driver.
For example: https://github.com/douglasdcm/caqui
"""

def __init__(self, driver):
super().__init__(driver)

async def do(self, **kwargs):
raise NotImplementedError(
"Selenium does not support asynchronous execution."
" Use your preferable async WebDriver. "
" For example https://github.com/douglasdcm/caqui"
)
70 changes: 70 additions & 0 deletions guara/asynchronous/transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging
from typing import Any, NoReturn
from selenium.webdriver.remote.webdriver import WebDriver
from guara.asynchronous.it import IAssertion

LOGGER = logging.getLogger(__name__)


class AbstractTransaction:
def __init__(self, driver: WebDriver):
self._driver = driver

async def do(self, **kwargs) -> Any | NoReturn:
raise NotImplementedError


class Application:
"""
This is the runner of the automation.
"""

def __init__(self, driver):
self._driver = driver
self._result = None
self._coroutines = []
self._TRANSACTION = "transaction"
self._ASSERTION = "assertion"

@property
def result(self):
return self._result

def at(self, transaction: AbstractTransaction, **kwargs):
"""It executes the `do` method of each transaction"""

LOGGER.info(f"Transaction '{transaction.__name__}'")
for k, v in kwargs.items():
LOGGER.info(f" {k}: {v}")

coroutine = transaction(self._driver).do(**kwargs)
self._coroutines.append({self._TRANSACTION: coroutine})

return self

def asserts(self, it: IAssertion, expected):
"""The `asserts` method receives a reference to an `IAssertion` instance.
It implements the `Strategy Pattern (GoF)` to allow its behavior to change at runtime.
It validates the result using the `asserts` method."""

LOGGER.info(f"Assertion '{it.__name__}'")
LOGGER.info(f" actual: '{self._result}'")
LOGGER.info(f" expected: '{expected}'")
LOGGER.info("---")

coroutine = it().asserts(self, expected)
self._coroutines.append({self._ASSERTION: coroutine})

return self

async def perform(self) -> "Application":
"""Executes the coroutines in order and saves the result of the transaction
in `result`"""

for coroutine in self._coroutines:
if coroutine.get(self._TRANSACTION):
self._result = await coroutine.get(self._TRANSACTION)
continue
await coroutine.get(self._ASSERTION)
self._coroutines.clear()
return self
7 changes: 7 additions & 0 deletions guara/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ def do(self, **kwargs) -> Any | NoReturn:


class Application:
"""This is the runner of the automation."""

def __init__(self, driver):
self._driver = driver
self._result = None

@property
def result(self):
return self._result

def at(self, transaction: AbstractTransaction, **kwargs):
"""It executes the `do` method of each transaction"""
LOGGER.info(f"Transaction '{transaction.__name__}'")
for k, v in kwargs.items():
LOGGER.info(f" {k}: {v}")
Expand All @@ -31,6 +35,9 @@ def at(self, transaction: AbstractTransaction, **kwargs):
return self

def asserts(self, it: IAssertion, expected):
"""The `asserts` method receives a reference to an `IAssertion` instance.
It implements the `Strategy Pattern (GoF)` to allow its behavior to change at runtime.
It validates the result using the `asserts` method."""
LOGGER.info(f"Assertion '{it.__name__}'")
LOGGER.info(f" actual: '{self._result}'")
LOGGER.info(f" expected: '{expected}'")
Expand Down
13 changes: 9 additions & 4 deletions tests/html/playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ <h1>Basic page</h1>
<input id="input">
<button id="button" onclick="myFunction(this, 'red')" class="my-class">test</button>
<br>
<a href="http://any1.com" id="a1">any1.com</a><br>
<a href="http://any2.com" id="a2">any2.com</a><br>
<a href="http://any3.com" id="a3">any3.com</a><br>
<a href="http://any4.com" id="a4">any4.com</a><br>
<div id="links"></p>
<script>
let text = "";
for (let i = 0; i < 20; i++) {
text += '<a href="http://any'+ i +'.com" id=a'+ i +'>any'+i+'.com</a><br>';
}

document.getElementById("links").innerHTML = text;
</script>
<br>
<button id="alert-button" onclick="openAlert()" any="any">alert</button>
<br>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from caqui import synchronous
from caqui import asynchronous
from guara.transaction import AbstractTransaction
from tests.web_ui_async.constants import MAX_INDEX


class GetNthLink(AbstractTransaction):
Expand All @@ -18,18 +19,18 @@ class GetNthLink(AbstractTransaction):
def __init__(self, driver):
super().__init__(driver)

def do(
async def do(
self,
link_index,
with_session,
connect_to_driver,
):
locator_type = "xpath"
locator_value = f"//a[@id='a{link_index}']"
anchor = synchronous.find_element(
anchor = await asynchronous.find_element(
connect_to_driver, with_session, locator_type, locator_value
)
return synchronous.get_text(connect_to_driver, with_session, anchor)
return await asynchronous.get_text(connect_to_driver, with_session, anchor)


class GetAllLinks(AbstractTransaction):
Expand All @@ -47,21 +48,18 @@ class GetAllLinks(AbstractTransaction):
def __init__(self, driver):
super().__init__(driver)

def do(self, with_session, connect_to_driver):
async def do(self, with_session, connect_to_driver):
links = []
MAX_INDEX = 4
max_index = MAX_INDEX - 1

for i in range(MAX_INDEX):
for i in range(max_index):
i += 1
links.append(
# Instead of duplicate the code it is possible to call transactions directly
GetNthLink(None).do(
await GetNthLink(None).do(
link_index=i,
with_session=with_session,
connect_to_driver=connect_to_driver,
)
)
# uncomment it to see the instances of the browser for a while
# import time
# time.sleep(2)
return links
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class OpenApp(AbstractTransaction):
def __init__(self, driver):
super().__init__(driver)

def do(self, with_session, connect_to_driver, access_url):
async def do(self, with_session, connect_to_driver, access_url):
synchronous.go_to_page(
connect_to_driver,
with_session,
Expand All @@ -43,6 +43,6 @@ class CloseApp(AbstractTransaction):
def __init__(self, driver):
super().__init__(driver)

def do(self, with_session, connect_to_driver):
synchronous.take_screenshot(connect_to_driver, with_session, "./captures/")
async def do(self, with_session, connect_to_driver):
synchronous.take_screenshot(connect_to_driver, with_session, "/tmp")
synchronous.close_session(connect_to_driver, with_session)
Loading

0 comments on commit 72a18d0

Please sign in to comment.