Merge branch 'chapter/09-shared-parts'

This commit is contained in:
Pandy Knight 2023-07-20 00:50:18 -04:00
commit a8f8ee6bc5
10 changed files with 209 additions and 291 deletions

15
Pipfile
View File

@ -1,15 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
pytest = "*"
pytest-bdd = "*"
requests = "*"
selenium = "*"
[requires]
python_version = "3.8"

203
Pipfile.lock generated
View File

@ -1,203 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "77217213ef2017abdfdad9b28716599c9f2748f9df7bab38aec5cb905509cc8b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"attrs": {
"hashes": [
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"version": "==21.2.0"
},
"certifi": {
"hashes": [
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
],
"version": "==2021.5.30"
},
"chardet": {
"hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==4.0.0"
},
"glob2": {
"hashes": [
"sha256:85c3dbd07c8aa26d63d7aacee34fa86e9a91a3873bc30bf62ec46e531f92ab8c"
],
"version": "==0.7"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
],
"version": "==1.1.1"
},
"mako": {
"hashes": [
"sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab",
"sha256:aea166356da44b9b830c8023cd9b557fa856bd8b4035d6de771ca027dfc5cc6e"
],
"version": "==1.1.4"
},
"markupsafe": {
"hashes": [
"sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
"sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
"sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
"sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
"sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
"sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
"sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
"sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
],
"version": "==2.0.1"
},
"packaging": {
"hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
],
"version": "==20.9"
},
"parse": {
"hashes": [
"sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"
],
"version": "==1.19.0"
},
"parse-type": {
"hashes": [
"sha256:089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e",
"sha256:7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"
],
"version": "==0.5.2"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"version": "==0.13.1"
},
"py": {
"hashes": [
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"version": "==1.10.0"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"version": "==2.4.7"
},
"pytest": {
"hashes": [
"sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634",
"sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"
],
"index": "pypi",
"version": "==6.2.3"
},
"pytest-bdd": {
"hashes": [
"sha256:74ea5a147ea558c99ae83d838e6acbe5c9e6843884a958f8231615d96838733d",
"sha256:982489f2f036c7561affe4eeb5b392a37e1ace2a9f260cad747b1c8119e63cfd"
],
"index": "pypi",
"version": "==4.0.2"
},
"requests": {
"hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"index": "pypi",
"version": "==2.25.1"
},
"selenium": {
"hashes": [
"sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
"sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"
],
"index": "pypi",
"version": "==3.141.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"version": "==1.16.0"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"version": "==0.10.2"
},
"urllib3": {
"hashes": [
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
],
"index": "pypi",
"version": "==1.26.5"
}
},
"develop": {}
}

179
README.md
View File

@ -1,40 +1,165 @@
# tau-pytest-bdd
This repository contains example code for the
*Behavior-Driven Python with pytest-bdd* course
from [Test Automation University](https://testautomationu.applitools.com/).
There is a branch for each chapter of the course showing the state of the code at the completion of the chapter.
## Version 4 Warning
The TAU course was developed with `pytest-bdd` version 3.
Unfortunately, **the version 4 update has incompatible changes.**
`@given` methods must now include a `target_fixture` parameter in order to work like pytest fixtures.
The TAU videos and transcripts use the old style of code,
but the example code here now uses the new style of code.
Below is an example of the new style of code needed:
```python
@given("the basket has 2 cucumbers", target_fixture='basket')
def basket():
return CucumberBasket(initial_count=2)
```
## Repository Purpose
## Setup
This project requires an up-to-date version of Python 3.
It also uses [pipenv](https://pipenv.readthedocs.io/) to manage packages.
The best way to learn pytest is through hands-on coding.
In the first lesson of the course, you will create a new Python project for test cases.
(That project will *not* be the same as this repository.)
Each chapter then introduces new concepts and provides coding instructions for your project.
If you code along with each chapter,
then you will have a complete, functioning test automation project by the end of the course.
This repository provides all example code for the project.
If you get stuck while building your own project,
compare your code to the example code in this repository.
However, try to avoid blindly copying code from the repository into your project.
Take the time to learn the concepts and code presented in each chapter.
## Repository Branches
* Each `chapter/*` branch contains the state of the code for each chapter
* The `main` branch contains the final state of the code at the end of the course
## Python Setup
Python setup can be complicated.
This section documents how to set up your machine for Python test automation development.
### Python Installation and Tools
You can complete this course using any OS: Windows, macOS, Linux, etc.
This course requires Python 3.
You can download the latest Python version from [Python.org](https://www.python.org/downloads/).
Follow the appropriate installation instructions for your operating system.
You should have basic Python programming skills before attempting this course.
Learning the language is always a prerequisite for learning automation.
If you need help learning Python, check out this article:
[How Do I Start Learning Python?](https://automationpanda.com/2020/02/18/how-do-i-start-learning-python/)
You should also have a good Python editor/IDE.
Good choices include [PyCharm](https://www.jetbrains.com/pycharm/)
and [Visual Studio Code](https://code.visualstudio.com/docs/languages/python).
You will also need [Git](https://git-scm.com/) if you want to clone this repository locally.
If you are new to Git, [try learning the basics](https://try.github.io/).
For Web UI tests, install the appropriate browser and WebDriver executable.
These tests use Firefox and [geckodriver](https://github.com/mozilla/geckodriver/releases).
### Python Installation Troubleshooting
Unfortunately, installing Python properly can be complicated,
especially if Python was previously installed on your machine.
To verify your Python installation, enter `python --version` at the command line.
You should see the proper version printed.
If the `python` command doesn't work or doesnt print the expected version number,
then try using the `python3` command instead.
If that still doesn't work,
then the correct Python installation might not be included in your system path.
Find the directory into which Python was installed,
manually add it to the system path,
relaunch the command line,
and try running Python again.
* [System Path Instructions for Windows](https://geek-university.com/python/add-python-to-the-windows-path/)
* [System Path Instructions for macOS](https://www.educative.io/edpresso/how-to-add-python-to-the-path-variable-in-mac)
* [System Path Instructions for Linux](https://www.computerhope.com/issues/ch001647.htm)
### Python Packages
This course will use a handful of third-party packages that are *not* part of the Python Standard Library.
They must be installed separately using `pip`, the standard Python package installer.
You can install them all before you create your test project,
or you can install them as you complete each chapter in the course.
To install each package, enter `pip install <package-name>` at the command line.
For example: `pip install pytest`.
If you already have a package installed but need to upgrade its version,
run `pip install --upgrade <package-name>`.
Please note that if you need to use the `python3` command to run Python,
then you might also need to use the `pip3` command in lieu of `pip`.
### Virtual Environments
Running `pip install` will install the pytest package globally for the whole system.
Installing Python packages globally is okay for this course,
but it typically isn't a best practice in the "read world."
Instead, each project should manage its own dependencies locally using a virtual environment.
Virtual environments let projects avoid unnecessary dependencies and version mismatches.
For simplicity, this course will not use or teach virtual environments.
If you would like to learn virtual environments on your own, then RealPython's article
[Python Virtual Environments: A Primer](https://realpython.com/python-virtual-environments-a-primer/)
is an excellent place to start.
### Package Versions
The [`requirements.txt`](requirements.txt) file contains the versions for each package used in this course.
To set up this project on your local machine:
1. Clone it from this GitHub repository.
2. Run `pipenv install` from the command line in the project's root directory.
3. For Web UI tests, install the appropriate browser and WebDriver executable.
* These tests use Firefox and [geckodriver](https://github.com/mozilla/geckodriver/releases).
## Running Tests
Run tests simply using the `pytest` command.
Depending upon your environment, it may be better to use `python -m pytest`.
If you are using `pipenv`, then run `pipenv run python -m pytest`.
Use the "-k" option to filter tests by tags.
## More Info
For the best experience, take the full TAU course chapter-by-chapter.
Check out the branch for each chapter and follow along.
To run the example tests from the command line, run `python -m pytest` from the project root directory.
This command will discover and run all tests in the project.
You can also run tests using the shorter `pytest` command.
However, I recommend always using the lengthier `python -m pytest` command.
The lengthier command automatically adds the current directory to `sys.path`
so that all modules in the project can be discovered.
The pytest command has several command line options.
Course material will cover many of them.
Check out the [Usage and Invocations](https://docs.pytest.org/en/stable/usage.html) page
for complete documentation.
*Warning:*
If you attempt to run tests from this example project,
make sure to checkout the correct branch first!
## Additional Resources
Python links:
* [Python.org](https://www.python.org/)
* [pytest.org](https://docs.pytest.org/)
* [How Do I Start Learning Python?](https://automationpanda.com/2020/02/18/how-do-i-start-learning-python/)
* [Python Virtual Environments: A Primer](https://realpython.com/python-virtual-environments-a-primer/)
* [Effective Python Testing with Pytest](https://realpython.com/pytest-python-testing/)
* [Automation Panda's Python Page](https://automationpanda.com/python/)
Books:
* [Python Testing with pytest](https://pragprog.com/titles/bopytest/) by Brian Okken
* [pytest Quick Start Guide](https://www.packtpub.com/web-development/pytest-quick-start-guide) by Bruno Oliveira
* [Test-Driven Development with Python](https://www.obeythetestinggoat.com/) by Harry J.W. Percival
Related TAU courses:
* [Python Programming](https://testautomationu.applitools.com/python-tutorial/)
* [Introduction to pytest](https://testautomationu.applitools.com/pytest-tutorial/)
* [Selenium WebDriver with Python](https://testautomationu.applitools.com/selenium-webdriver-python-tutorial/)
* [Automated Visual Testing with Python](https://testautomationu.applitools.com/visual-testing-python/)
## About the Author
This course was written and delivered by **Andrew Knight** (aka *Pandy*), the "Automation Panda".
Andy is a Pythonista who specializes in testing and automation.
* Blog: [AutomationPanda.com](https://automationpanda.com/)
* Twitter: [@AutomationPanda](https://twitter.com/AutomationPanda)
* LinkedIn: [andrew-leland-knight](https://www.linkedin.com/in/andrew-leland-knight/)

View File

@ -1,5 +1,4 @@
[pytest]
junit_family = xunit2
markers =
add
cucumber-basket
@ -7,4 +6,4 @@ markers =
remove
service
web
testpaths = tests
testpaths = tests

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
pytest==7.4.0
pytest-bdd==6.1.1
requests==2.31.0
selenium==4.10.0

View File

@ -20,4 +20,4 @@ Feature: DuckDuckGo Instant Answer API
| phrase |
| peach |
| pineapple |
| papaya |
| papaya |

View File

@ -32,4 +32,4 @@ def browser():
@given('the DuckDuckGo home page is displayed', target_fixture='ddg_home')
def ddg_home(browser):
browser.get(DUCKDUCKGO_HOME)
browser.get(DUCKDUCKGO_HOME)

View File

@ -3,40 +3,40 @@ from pytest_bdd import scenarios, parsers, given, when, then
from cucumbers import CucumberBasket
EXTRA_TYPES = {
'Number': int,
}
scenarios('../features/cucumbers.feature')
CONVERTERS = {
'initial': int,
'some': int,
'total': int,
'initial': int,
'some': int,
'total': int,
}
scenarios('../features/cucumbers.feature', example_converters=CONVERTERS)
@given(parsers.cfparse('the basket has "{initial:Number}" cucumbers', extra_types=EXTRA_TYPES), target_fixture='basket')
@given('the basket has "<initial>" cucumbers', target_fixture='basket')
@given(
parsers.parse('the basket has "{initial}" cucumbers'),
target_fixture='basket',
converters=CONVERTERS)
def basket(initial):
return CucumberBasket(initial_count=initial)
return CucumberBasket(initial_count=initial)
@when(parsers.cfparse('"{some:Number}" cucumbers are added to the basket', extra_types=EXTRA_TYPES))
@when('"<some>" cucumbers are added to the basket')
@when(
parsers.parse('"{some}" cucumbers are added to the basket'),
converters=CONVERTERS)
def add_cucumbers(basket, some):
basket.add(some)
basket.add(some)
@when(parsers.cfparse('"{some:Number}" cucumbers are removed from the basket', extra_types=EXTRA_TYPES))
@when('"<some>" cucumbers are removed from the basket')
@when(
parsers.parse('"{some}" cucumbers are removed from the basket'),
converters=CONVERTERS)
def remove_cucumbers(basket, some):
basket.remove(some)
basket.remove(some)
@then(parsers.cfparse('the basket contains "{total:Number}" cucumbers', extra_types=EXTRA_TYPES))
@then('the basket contains "<total>" cucumbers')
@then(
parsers.parse('the basket contains "{total}" cucumbers'),
converters=CONVERTERS)
def basket_has_total(basket, total):
assert basket.count == total
assert basket.count == total

View File

@ -16,26 +16,39 @@ DUCKDUCKGO_API = 'https://api.duckduckgo.com/'
# Scenarios
scenarios('../features/service.feature', example_converters=dict(phrase=str))
scenarios('../features/service.feature')
CONVERTERS = {
'code': int,
'phrase': str,
}
# Given Steps
@given('the DuckDuckGo API is queried with "<phrase>"', target_fixture='ddg_response')
@given(
parsers.parse('the DuckDuckGo API is queried with "{phrase}"'),
target_fixture='ddg_response',
converters=CONVERTERS)
def ddg_response(phrase):
params = {'q': phrase, 'format': 'json'}
response = requests.get(DUCKDUCKGO_API, params=params)
return response
params = {'q': phrase, 'format': 'json'}
response = requests.get(DUCKDUCKGO_API, params=params)
return response
# Then Steps
@then('the response contains results for "<phrase>"')
@then(
parsers.parse('the response contains results for "{phrase}"'),
converters=CONVERTERS)
def ddg_response_contents(ddg_response, phrase):
# A more comprehensive test would check 'RelatedTopics' for matching phrases
assert phrase.lower() == ddg_response.json()['Heading'].lower()
# A more comprehensive test would check 'RelatedTopics' for matching phrases
assert phrase.lower() == ddg_response.json()['Heading'].lower()
@then(parsers.parse('the response status code is "{code:d}"'))
@then(
parsers.parse('the response status code is "{code}"'),
converters=CONVERTERS)
def ddg_response_code(ddg_response, code):
assert ddg_response.status_code == code
assert ddg_response.status_code == code

View File

@ -12,14 +12,10 @@ Prerequisites:
"""
from pytest_bdd import scenarios, when, then, parsers
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
# Constants
DUCKDUCKGO_HOME = 'https://duckduckgo.com/'
# Scenarios
scenarios('../features/web.feature')
@ -30,7 +26,7 @@ scenarios('../features/web.feature')
@when(parsers.parse('the user searches for "{text}"'))
@when(parsers.parse('the user searches for the phrase:\n{text}'))
def search_phrase(browser, text):
search_input = browser.find_element_by_id('search_form_input_homepage')
search_input = browser.find_element(By.NAME, 'q')
search_input.send_keys(text + Keys.RETURN)
@ -38,8 +34,8 @@ def search_phrase(browser, text):
@then(parsers.parse('one of the results contains "{phrase}"'))
def results_have_one(browser, phrase):
xpath = "//div[@id='links']//*[contains(text(), '%s')]" % phrase
results = browser.find_elements_by_xpath(xpath)
xpath = "//*[@data-testid='result']//*[contains(text(), '%s')]" % phrase
results = browser.find_elements(By.XPATH, xpath)
assert len(results) > 0
@ -48,8 +44,7 @@ def search_results(browser, phrase):
# Check search result list
# (A more comprehensive test would check results for matching phrases)
# (Check the list before the search phrase for correct implicit waiting)
links_div = browser.find_element_by_id('links')
assert len(links_div.find_elements_by_xpath('//div')) > 0
assert len(browser.find_elements(By.CSS_SELECTOR, '[data-testid="result"]')) > 0
# Check search phrase
search_input = browser.find_element_by_id('search_form_input')
search_input = browser.find_element(By.NAME, 'q')
assert search_input.get_attribute('value') == phrase