├── Hands-On Web UI Testing Slides.pdf
├── tests
└── test_fw.py
├── Pipfile
├── LICENSE
├── .gitignore
├── Pipfile.lock
└── README.md
/Hands-On Web UI Testing Slides.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AndyLPK247/djangocon-2019-web-ui-testing/HEAD/Hands-On Web UI Testing Slides.pdf
--------------------------------------------------------------------------------
/tests/test_fw.py:
--------------------------------------------------------------------------------
1 | """
2 | Run these tests after completing the setup steps to verify that the framework works.
3 | """
4 |
5 | def test_the_tests():
6 | assert True
7 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 |
8 | [packages]
9 | pytest = "*"
10 |
11 | [requires]
12 | python_version = "3.7"
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Andrew Knight
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | # IDEs
107 | .vscode
108 | .idea
109 | *.iml
110 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "828b8ad012f4c8773e6e61e3ac2be0ffcd7540fd7ed175a8355676c8e31c4d3d"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.7"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "atomicwrites": {
20 | "hashes": [
21 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
22 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
23 | ],
24 | "version": "==1.3.0"
25 | },
26 | "attrs": {
27 | "hashes": [
28 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
29 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
30 | ],
31 | "version": "==19.1.0"
32 | },
33 | "importlib-metadata": {
34 | "hashes": [
35 | "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8",
36 | "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"
37 | ],
38 | "markers": "python_version < '3.8'",
39 | "version": "==0.19"
40 | },
41 | "more-itertools": {
42 | "hashes": [
43 | "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
44 | "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
45 | ],
46 | "version": "==7.2.0"
47 | },
48 | "packaging": {
49 | "hashes": [
50 | "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9",
51 | "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"
52 | ],
53 | "version": "==19.1"
54 | },
55 | "pluggy": {
56 | "hashes": [
57 | "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
58 | "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
59 | ],
60 | "version": "==0.12.0"
61 | },
62 | "py": {
63 | "hashes": [
64 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
65 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
66 | ],
67 | "version": "==1.8.0"
68 | },
69 | "pyparsing": {
70 | "hashes": [
71 | "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
72 | "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
73 | ],
74 | "version": "==2.4.2"
75 | },
76 | "pytest": {
77 | "hashes": [
78 | "sha256:95b1f6db806e5b1b5b443efeb58984c24945508f93a866c1719e1a507a957d7c",
79 | "sha256:c3d5020755f70c82eceda3feaf556af9a341334414a8eca521a18f463bcead88"
80 | ],
81 | "index": "pypi",
82 | "version": "==5.1.1"
83 | },
84 | "six": {
85 | "hashes": [
86 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
87 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
88 | ],
89 | "version": "==1.12.0"
90 | },
91 | "wcwidth": {
92 | "hashes": [
93 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
94 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
95 | ],
96 | "version": "==0.1.7"
97 | },
98 | "zipp": {
99 | "hashes": [
100 | "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
101 | "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
102 | ],
103 | "version": "==0.6.0"
104 | }
105 | },
106 | "develop": {}
107 | }
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # djangocon-2019-web-ui-testing
2 | This repository contains the companion project for the
3 | [Hands-On Web UI Testing](https://2019.djangocon.us/tutorials/hands-on-web-ui-testing/) tutorial
4 | taught by [Pandy Knight](https://twitter.com/AutomationPanda)
5 | at [DjangoCon 2019](https://2019.djangocon.us/).
6 | If you are taking the tutorial,
7 | then please clone this repository and follow the instructions below.
8 | The slide deck for the tutorial is also in this repository.
9 |
10 | ## WARNING!
11 |
12 | **This project is archived.**
13 | It was the example code for a one-time workshop, and it will not be actively maintained.
14 | If you want to learn how to do Web UI testing with Python and Selenium WebDriver,
15 | then please take [Selenium WebDriver with Python](https://testautomationu.applitools.com/selenium-webdriver-python-tutorial/)
16 | from [Test Automation University](https://testautomationu.applitools.com/).
17 | It is a free course that covers the same material as this tutorial.
18 |
19 | ## Python Setup
20 |
21 | You can complete this tutorial using any OS: Windows, macOS, Linux, etc.
22 |
23 | This tutorial requires Python 3.7 or higher.
24 | You can download the latest Python version from [Python.org](https://www.python.org/downloads/).
25 |
26 | This tutorial also requires [pipenv](https://docs.pipenv.org/).
27 | To install pipenv, run `pip install pipenv` from the command line.
28 |
29 | You should also have a Python editor/IDE of your choice.
30 | Good choices include [PyCharm](https://www.jetbrains.com/pycharm/)
31 | and [Visual Studio Code](https://code.visualstudio.com/docs/languages/python).
32 |
33 | You will also need [Git](https://git-scm.com/) to copy this project code.
34 | If you are new to Git, [try learning the basics](https://try.github.io/).
35 |
36 | ## WebDriver Setup
37 |
38 | For Web UI testing, you will need to install the latest versions of
39 | [Google Chrome](https://www.google.com/chrome/)
40 | and [Mozilla Firefox](https://www.mozilla.org/en-US/firefox/).
41 | You can use other browsers with Selenium WebDriver, but the tutorial will use Chrome and Firefox.
42 |
43 | You will also need to install the latest versions of the WebDriver executables for these browsers: [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/) for Chrome
44 | and [geckodriver](https://github.com/mozilla/geckodriver/releases) for Firefox.
45 | Each test case will launch the WebDriver executable for its target browser.
46 | The WebDriver executable will act as a proxy between the test automation and the browser instance.
47 | Please use the latest versions of both the browsers and the WebDriver executables.
48 | Older versions might be incompatible with each other.
49 |
50 | ChromeDriver and geckodriver must be installed on the
51 | [system path](https://en.wikipedia.org/wiki/PATH_(variable)).
52 |
53 | To install them on Windows:
54 |
55 | 1. Create a folder named `C:\Selenium`.
56 | 2. Move the executables into this folder.
57 | 3. Add this folder to the *Path* environment variable. (See [How to Add to Windows PATH Environment Variable](https://helpdeskgeek.com/windows-10/add-windows-path-environment-variable/).)
58 |
59 | To install them on Linux, macOS, and other UNIX variants,
60 | simply move them to the `/usr/local/bin/` directory:
61 |
62 | ```bash
63 | $ mv /path/to/ChromeDriver /usr/local/bin
64 | $ mv /path/to/geckodriver /usr/local/bin
65 | ```
66 |
67 | This directory should already be included in the system path.
68 | For troubleshooting, see:
69 |
70 | * [Setting the path on macOS](https://www.cyberciti.biz/faq/appleosx-bash-unix-change-set-path-environment-variable/)
71 | * [Setting the path on Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
72 |
73 | To verify correct setup on any operating system, simply try to run them from the terminal:
74 |
75 | ```bash
76 | $ ChromeDriver
77 | $ geckodriver
78 | ```
79 |
80 | You may or may not see any output.
81 | Just verify that you can run them without errors.
82 | Use Ctrl-C to kill them.
83 |
84 | ## Project Setup
85 |
86 | 1. Clone this repository.
87 | 2. Run `cd djangocon-2019-web-ui-testing` to enter the project.
88 | 3. Run `pipenv install` to install the dependencies.
89 | 4. Run `pipenv run python -m pytest` to verify that the framework can run tests.
90 | 5. Create a branch for your code changes. (See *Repository Branching* below.)
91 |
92 | ## Repository Branching
93 |
94 | The `master` branch contains the code for the tutorial's starting point.
95 | The project is basically empty in the `master` branch.
96 |
97 | If you want to code along with the tutorial, then create a branch for your work off the `master` branch.
98 | To create your own branch named `tutorial/develop`, run:
99 |
100 | > git checkout master
101 | > git branch tutorial/develop
102 | > git checkout tutorial/develop
103 |
104 | The `example/*` branches contain the completed code for tutorial parts.
105 | If you get stuck, you can always check the example code.
106 |
107 | * `example/1-first-test`
108 | * `example/2-webdriver-setup`
109 | * `example/3-page-objects`
110 | * `example/4-locators`
111 | * `example/5-webdriver-calls`
112 | * `example/6-browser-config`
113 | * `example/7-race-conditions`
114 | * `example/8-parallel-testing`
115 | * `example/develop` (main development branch for the examples)
116 |
117 | ## Tutorial Instructions
118 |
119 | ### Part 1: Writing Our First Test
120 |
121 | *Time Estimate: 5 Minutes*
122 |
123 | *Example Branch: example/1-first-test*
124 |
125 | We should always write test *cases* before writing any test *code*.
126 | Test cases are procedures that exercise behavior to verify goodness and identify badness.
127 | Test code simply automates test cases.
128 | Writing a test case first helps us form our thoughts well.
129 |
130 | Consider the following test case:
131 |
132 | ```gherkin
133 | Scenario: Basic DuckDuckGo Search
134 | Given the DuckDuckGo home page is displayed
135 | When the user searches for “panda”
136 | Then the search result title contains “panda”
137 | And the search result query is “panda”
138 | And the search result links pertain to “panda”
139 | ```
140 |
141 | Let's implement this test using pytest.
142 | Create a new file named `test_search.py` under the `tests` directory,
143 | and add the following code:
144 |
145 | ```python
146 | """
147 | These tests cover DuckDuckGo searches.
148 | """
149 |
150 | def test_basic_duckduckgo_search():
151 |
152 | # Given the DuckDuckGo home page is displayed
153 | # TODO
154 |
155 | # When the user searches for "panda"
156 | # TODO
157 |
158 | # Then the search result title contains "panda"
159 | # TODO
160 |
161 | # And the search result query is "panda"
162 | # TODO
163 |
164 | # And the search result links pertain to "panda"
165 | # TODO
166 |
167 | raise Exception("Incomplete Test")
168 | ```
169 |
170 | Adding comments to stub each step may seem trivial,
171 | but it's a good first step when writing new test cases.
172 | We can simply add code at each TODO line as we automate.
173 | Once we finish writing the test's code, we will remove the exception at the end.
174 | Also, note that pytest expects all test functions to begin with `test_`.
175 |
176 | To avoid confusion when we run tests, let's remove the old placeholder test.
177 | Delete `tests/test_fw.py`.
178 |
179 | Rerun the tests using `pipenv run python -m pytest`.
180 | The `test_basic_duckduckgo_search` should be the only test that runs,
181 | and it should fail due to the "Incomplete Test" exception.
182 |
183 | Finally, commit your code change. Part 1 is complete!
184 |
185 | ### Part 2: Setting Up Selenium WebDriver
186 |
187 | *Time Estimate: 5 Minutes*
188 |
189 | *Example Branch: example/2-webdriver-setup*
190 |
191 | [Selenium WebDriver](https://www.seleniumhq.org/projects/webdriver/)
192 | is a tool for automating Web UI interactions with live browsers.
193 | It works with several popular programming languages and browser types.
194 |
195 | The Selenium WebDriver package for Python is named `selenium`.
196 | Run `pipenv install selenium` to install it for our project.
197 |
198 | Every test should use its own WebDriver instance.
199 | This keeps things simple and safe.
200 | The best way to set up the WebDriver instance is to use a
201 | [pytest fixture](https://docs.pytest.org/en/latest/fixture.html).
202 | Fixtures are basically setup and cleanup functions.
203 | As a best practice, they should be placed in a `conftest.py` module so they can be used by any test.
204 |
205 | Create a new file named `tests/conftest.py` and add the following code:
206 |
207 | ```python
208 | """
209 | This module contains shared fixtures.
210 | """
211 |
212 | import pytest
213 | import selenium.webdriver
214 |
215 |
216 | @pytest.fixture
217 | def browser():
218 |
219 | # Initialize the ChromeDriver instance
220 | b = selenium.webdriver.Chrome()
221 |
222 | # Make its calls wait up to 10 seconds for elements to appear
223 | b.implicitly_wait(10)
224 |
225 | # Return the WebDriver instance for the setup
226 | yield b
227 |
228 | # Quit the WebDriver instance for the cleanup
229 | b.quit()
230 | ```
231 |
232 | The `browser` fixture uses Chrome.
233 | Other browser types could be used instead.
234 | Real-world projects often read browser choice from a config file here.
235 |
236 | The implicit wait will make sure WebDriver calls wait for elements to appear before sending calls to them.
237 | 10 seconds should be reasonable for this test project's needs.
238 | For larger projects, however, setting explicit waits is a better practice
239 | because different calls need different wait times.
240 | Read more about implicit versus explicit waits [here](https://selenium-python.readthedocs.io/waits.html).
241 |
242 | The `yield` statement makes the `browser` fixture a generator.
243 | The first iteration will do the "setup" steps,
244 | while the second iteration will do the "cleanup" steps.
245 | Each test must make sure to *quit* the WebDriver instance as part of cleanup,
246 | or else zombie processes might lock system resources!
247 |
248 | Now, update `test_basic_duckduckgo_search` in `tests/test_search.py` to call the new fixture:
249 |
250 | ```python
251 | def test_basic_duckduckgo_search(browser):
252 | # ...
253 | ```
254 |
255 | Whenever a pytest test function declares a fixture by name as an argument,
256 | pytest will automatically call that fixture before the test runs.
257 | Whatever the fixture returns will be passed into the test function.
258 | Therefore, we can access the WebDriver instance using the `browser` variable!
259 |
260 | Rerun the test using `pipenv run python -m pytest` to test the fixture.
261 | Even though the test should still fail,
262 | Chrome should briefly pop up for a few seconds while the test is running.
263 | Make sure Chrome quits once the test is done.
264 | Then, commit your latest code changes.
265 | Part 2 is now complete!
266 |
267 | ### Part 3: Defining Page Objects
268 |
269 | *Time Estimate: 10 Minutes*
270 |
271 | *Example Branch: example/3-page-objects*
272 |
273 | A **page object** is an object representing a Web page or component.
274 | They have *locators* for finding elements,
275 | as well as *interaction methods* that interact with the page under test.
276 | Page objects make low-level Selenium WebDriver calls
277 | so that tests can make short, readable calls instead of complex ones.
278 |
279 | Since we have our test steps, we know what pages and elements our test needs.
280 | There are two pages under test, each with a few interactions:
281 |
282 | 1. The DuckDuckGo search page
283 | * Load the page
284 | * Search a phrase
285 | 2. The DuckDuckGo results page
286 | * Get the result link titles
287 | * Get the search query
288 | * Get the title
289 |
290 | Let's write stubs for our page object classes.
291 | Each interaction should have its own method.
292 | Later, we can implement the interaction methods with Selenium WebDriver calls.
293 | Create a new Python package named `pages`.
294 | To do this create a directory under the root directory named `pages`.
295 | Then, put a blank file in it named `__init__.py`.
296 | The `pages` directory should *not* be under the `tests` directory.
297 | Why? When using pytest, the `tests` folder should *not* be a package.
298 |
299 | Create a new module named `pages/search.py` and add the following code
300 | for the DuckDuckGo search page:
301 |
302 | ```python
303 | """
304 | This module contains DuckDuckGoSearchPage,
305 | the page object for the DuckDuckGo search page.
306 | """
307 |
308 |
309 | class DuckDuckGoSearchPage:
310 |
311 | def __init__(self, browser):
312 | self.browser = browser
313 |
314 | def load(self):
315 | # TODO
316 | pass
317 |
318 | def search(self, phrase):
319 | # TODO
320 | pass
321 | ```
322 |
323 | Create another new module named `pages/result.py` and add the following code
324 | for the DuckDuckGo result page:
325 |
326 | ```python
327 | """
328 | This module contains DuckDuckGoResultPage,
329 | the page object for the DuckDuckGo search result page.
330 | """
331 |
332 |
333 | class DuckDuckGoResultPage:
334 |
335 | def __init__(self, browser):
336 | self.browser = browser
337 |
338 | def result_link_titles(self):
339 | # TODO
340 | return []
341 |
342 | def search_input_value(self):
343 | # TODO
344 | return ""
345 |
346 | def title(self):
347 | # TODO
348 | return ""
349 | ```
350 |
351 | Every page object needs a reference to the WebDriver instance.
352 | That's why the `__init__` methods take in and store a reference to `browser`.
353 |
354 | Finally, update `test_basic_duckduckgo_search` in `tests/test_search.py`
355 | with the following code:
356 |
357 | ```python
358 | """
359 | These tests cover DuckDuckGo searches.
360 | """
361 |
362 | from pages.result import DuckDuckGoResultPage
363 | from pages.search import DuckDuckGoSearchPage
364 |
365 |
366 | def test_basic_duckduckgo_search(browser):
367 | search_page = DuckDuckGoSearchPage(browser)
368 | result_page = DuckDuckGoResultPage(browser)
369 | PHRASE = "panda"
370 |
371 | # Given the DuckDuckGo home page is displayed
372 | search_page.load()
373 |
374 | # When the user searches for "panda"
375 | search_page.search(PHRASE)
376 |
377 | # Then the search result title contains "panda"
378 | assert PHRASE in result_page.title()
379 |
380 | # And the search result query is "panda"
381 | assert PHRASE == result_page.search_input_value()
382 |
383 | # And the search result links pertain to "panda"
384 | for title in result_page.result_link_titles():
385 | assert PHRASE.lower() in title.lower()
386 |
387 | # TODO: Remove this exception once the test is complete
388 | raise Exception("Incomplete Test")
389 | ```
390 |
391 | Notice how we are able to write all the test steps using page object calls and assertions.
392 | We also kept the step comments so the code is well-documented.
393 | Even though we haven't made any Selenium WebDriver calls, our test case function is nearly complete!
394 | Our code is readable and understandable.
395 | It delivers clear testing value.
396 |
397 | Rerun the test using `pipenv run python -m pytest`.
398 | The test should fail again, but this time, it should fail on one of the assertions.
399 | Then, commit your latest code changes. Part 3 is now complete!
400 |
401 | ### Part 4: Finding Locators for Elements
402 |
403 | *Time Estimate: 15 Minutes*
404 |
405 | *Example Branch: example/4-locators*
406 |
407 | An *element* is a "thing" on a Web page.
408 | Browsers render elements such as buttons, dropdowns, and input fields using the page's HTML code.
409 | Users interact directly with the page's elements.
410 | Tests use page objects to interact with elements like a user.
411 |
412 | Interactions typically require three steps:
413 |
414 | 1. Wait for the target element to exist
415 | 2. Get an object representing the target element
416 | 3. Send commands to the element object
417 |
418 | In our solution, waiting is handled automatically thanks to the browser fixture's `implicitly_wait` call.
419 | Getting the element object, however, requires a locator.
420 |
421 | *Locators* are query strings that use HTML attributes to find elements on a Web page.
422 | There are many types of locators:
423 |
424 | * ID
425 | * Name
426 | * Class name
427 | * CSS Selector
428 | * XPath
429 | * Link text
430 | * Partial link text
431 | * Tag name
432 |
433 | For example, if the page has the following element:
434 |
435 | ```html
436 |
437 | ```
438 |
439 | Then, a page object could use an ID locator for "django_ok" to get this element.
440 |
441 | Locators are not element objects themselves but instead point to elements.
442 | The WebDriver instance uses locators to fetch and construct element objects.
443 | Why are locators and elements separate concerns?
444 | Elements on a page are always changing:
445 | they may take time to load, or they may change with user interaction.
446 | Locators, however, are always the same:
447 | they simply specify how to get elements.
448 | For example, a locator could be used to prove that an element does *not* exist.
449 |
450 | For our test, we need locators for three elements:
451 |
452 | 1. The search input on the DuckDuckGo search page
453 | 2. The search input on the DuckDuckGo results page
454 | 3. The result links on the DuckDuckGo results page
455 |
456 | (Note: The page title is not a Web element. It can be fetched as a `browser` property.)
457 |
458 | Writing good locators is a bit of an art.
459 | Inspecting the HTML source of a live page makes it easy.
460 | To do this, open the [DuckDuckGo search page](https://duckduckgo.com/) in Chrome.
461 | Then, right-click the page and select "Inspect".
462 | [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/) will open.
463 | The "Elements" tab shows the HTML source.
464 | As you move the cursor over HTML elements in the source,
465 | Chrome will highlight the elements on the page.
466 | Now, click the icon with the box and cursor in the upper-left corner of the DevTools pane.
467 | Move the cursor over elements on the page, and you will see them highlighted in the source.
468 | Neat!
469 |
470 | Try to find the search input element.
471 | Its HTML should look like this:
472 |
473 | ```html
474 |
484 | ```
485 |
486 | Notice that there is an `id` attribute set to "search_form_input_homepage".
487 | Let's use this as our locator and update `pages/search.py`.
488 |
489 | First, import `By` from the `selenium` package so we can write locators:
490 |
491 | ```python
492 | from selenium.webdriver.common.by import By
493 | ```
494 |
495 | Then, add the following attribute to the `DuckDuckGoSearchPage` class:
496 |
497 | ```python
498 | SEARCH_INPUT = (By.ID, 'search_form_input_homepage')
499 | ```
500 |
501 | `By` contains property keys for each type of locator.
502 | We can write locators as tuples of the locator type and the query string.
503 | (We will use this locator for interaction calls in the next part of the tutorial.)
504 |
505 | The full code for `pages/search.py` should now look like this:
506 |
507 | ```python
508 | """
509 | This module contains DuckDuckGoSearchPage,
510 | the page object for the DuckDuckGo search page.
511 | """
512 |
513 | from selenium.webdriver.common.by import By
514 |
515 |
516 | class DuckDuckGoSearchPage:
517 |
518 | SEARCH_INPUT = (By.ID, 'search_form_input_homepage')
519 |
520 | def __init__(self, browser):
521 | self.browser = browser
522 |
523 | def load(self):
524 | # TODO
525 | pass
526 |
527 | def search(self, phrase):
528 | # TODO
529 | pass
530 | ```
531 |
532 | Let's write locators for the `DuckDuckGoResultPage` next.
533 | Perform a search, inspect the page, and try to come up with locators on your own.
534 |
535 | Below is the code for `pages/result.py` with locators:
536 |
537 | ```python
538 | """
539 | This module contains DuckDuckGoResultPage,
540 | the page object for the DuckDuckGo search result page.
541 | """
542 |
543 | from selenium.webdriver.common.by import By
544 |
545 |
546 | class DuckDuckGoResultPage:
547 |
548 | RESULT_LINKS = (By.CSS_SELECTOR, 'a.result__a')
549 | SEARCH_INPUT = (By.ID, 'search_form_input')
550 |
551 | def __init__(self, browser):
552 | self.browser = browser
553 |
554 | def result_link_titles(self):
555 | # TODO
556 | return []
557 |
558 | def search_input_value(self):
559 | # TODO
560 | return ""
561 |
562 | def title(self):
563 | # TODO
564 | return ""
565 | ```
566 |
567 | Thankfully, the search input element on the result page
568 | is similar to the one on the search page.
569 |
570 | The locator for the result links is a bit trickier, though.
571 | It must find all result elements that contain the search phrase in their display texts.
572 | This locator will return a list of elements, not just one.
573 | The result links are all "a" hyperlink elements with a class named "result__a".
574 | We can use a CSS selector for its query.
575 |
576 | We should always try to use the simplest locator that uniquely finds the target elements.
577 | IDs, names, and class names are the easiest,
578 | but sometimes, we must use CSS selectors and XPaths.
579 | To learn more about writing good locators,
580 | take the [Web Element Locator Strategies](https://testautomationu.applitools.com/web-element-locator-strategies/) course
581 | from [Test Automation University](https://testautomationu.applitools.com/).
582 |
583 | Although the test will still fail,
584 | rerun it using `pipenv run python -m pytest` to make sure our changes did no harm.
585 | Then, commit your latest code changes. Part 4 is now complete!
586 |
587 | ### Part 5: Making WebDriver Calls
588 |
589 | *Time Estimate: 15 Minutes*
590 |
591 | *Example Branch: example/5-webdriver-calls*
592 |
593 | Now we can implement all the page object methods using WebDriver calls.
594 | The [WebDriver API for Python](https://selenium-python.readthedocs.io/api.html)
595 | documents all WebDriver calls.
596 | If you aren't sure how to do something, look it up.
597 | WebDriver can do anything a user can do on a Web page!
598 |
599 | Let's start with `DuckDuckGoSearchPage`.
600 | The `load` method is a one-line WebDriver call,
601 | but it's good practice to make the URL a class variable:
602 |
603 | ```python
604 | URL = 'https://www.duckduckgo.com'
605 |
606 | def load(self):
607 | self.browser.get(self.URL)
608 | ```
609 |
610 | The `search` method is a bit more complex because it interacts with an element.
611 | We need to use a *locator* to find the search input element,
612 | and then we need to *send keys* to type the search phrase into the element.
613 |
614 | First, update the `selenium` package imports:
615 |
616 | ```python
617 | from selenium.webdriver.common.by import By
618 | from selenium.webdriver.common.keys import Keys
619 | ```
620 |
621 | The `search` method needs two parts: finding the element and sending the keystrokes.
622 | Thankfully, we already have the locator for the element.
623 |
624 | ```python
625 | def search(self, phrase):
626 | search_input = self.browser.find_element(*self.SEARCH_INPUT)
627 | search_input.send_keys(phrase + Keys.RETURN)
628 | ```
629 |
630 | The `find_element` method will return the first element found by the locator.
631 | Notice how the locator uses the `*` operator to expand the SEARCH_INPUT locator tuple into arguments.
632 | The `selenium` package offers specific locator type methods (like `find_element_by_name`),
633 | but using the generic `find_element` method with argument expansion is better practice.
634 | If the locator type must be changed due to Web page updates,
635 | then the `find_element` call would not need to be changed.
636 |
637 | The `send_keys` method sends the search phrase passed into the `search` method.
638 | This means that the page object can search any phrase!
639 | The addition of `Keys.RETURN` will send the ENTER/RETURN key as well,
640 | which will submit the input value to perform the search and load the results page.
641 |
642 | The full code for `pages/search.py` should look like this:
643 |
644 | ```python
645 | """
646 | This module contains DuckDuckGoSearchPage,
647 | the page object for the DuckDuckGo search page.
648 | """
649 |
650 | from selenium.webdriver.common.by import By
651 | from selenium.webdriver.common.keys import Keys
652 |
653 |
654 | class DuckDuckGoSearchPage:
655 |
656 | # URL
657 |
658 | URL = 'https://www.duckduckgo.com'
659 |
660 | # Locators
661 |
662 | SEARCH_INPUT = (By.ID, 'search_form_input_homepage')
663 |
664 | # Initializer
665 |
666 | def __init__(self, browser):
667 | self.browser = browser
668 |
669 | # Interaction Methods
670 |
671 | def load(self):
672 | self.browser.get(self.URL)
673 |
674 | def search(self, phrase):
675 | search_input = self.browser.find_element(*self.SEARCH_INPUT)
676 | search_input.send_keys(phrase + Keys.RETURN)
677 | ```
678 |
679 | Now, let's do `DuckDuckGoResultPage`.
680 | The `title` method is the easiest one because it just returns a property value:
681 |
682 | ```python
683 | def title(self):
684 | return self.browser.title
685 | ```
686 |
687 | The `search_input_value` method is similar to the `search` method from `DuckDuckGoSearchPage`,
688 | but instead of sending a command, it asks for state from the page.
689 | The "value" attribute contains the text a user types into an "input" element.
690 |
691 | ```python
692 | def search_input_value(self):
693 | search_input = self.browser.find_element(*self.SEARCH_INPUT)
694 | value = search_input.get_attribute('value')
695 | return value
696 | ```
697 |
698 | The `result_link_titles` method is a bit more complex.
699 | The test must verify that the result page displays links relating to the search phrase.
700 | This method should find all result links on the page.
701 | Then, it should get the titles for those result links.
702 | Remember, the test asserts that the search phrase is in each title.
703 | This assertion may seem too stringent because it could fail the test for possibly valid links,
704 | but it should be good enough for simple search terms.
705 | (Again, remember, the test is merely a basic search test.)
706 |
707 | The `result_link_titles` method should look like this:
708 |
709 | ```python
710 | def result_link_titles(self):
711 | links = self.browser.find_elements(*self.RESULT_LINKS)
712 | titles = [link.text for link in links]
713 | return titles
714 | ```
715 |
716 | Notice that it uses `find_elements` (plural) to get a list of matching elements.
717 |
718 | The full code for `pages/result.py` should look like this:
719 |
720 | ```python
721 | """
722 | This module contains DuckDuckGoResultPage,
723 | the page object for the DuckDuckGo search result page.
724 | """
725 |
726 | from selenium.webdriver.common.by import By
727 |
728 |
729 | class DuckDuckGoResultPage:
730 |
731 | # Locators
732 |
733 | RESULT_LINKS = (By.CSS_SELECTOR, 'a.result__a')
734 | SEARCH_INPUT = (By.ID, 'search_form_input')
735 |
736 | # Initializer
737 |
738 | def __init__(self, browser):
739 | self.browser = browser
740 |
741 | # Interaction Methods
742 |
743 | def result_link_titles(self):
744 | links = self.browser.find_elements(*self.RESULT_LINKS)
745 | titles = [link.text for link in links]
746 | return titles
747 |
748 | def search_input_value(self):
749 | search_input = self.browser.find_element(*self.SEARCH_INPUT)
750 | value = search_input.get_attribute('value')
751 | return value
752 |
753 | def title(self):
754 | return self.browser.title
755 | ```
756 |
757 | Finally, remove the "incomplete" exception from `tests/test_search.py`.
758 | That module's code should look like this:
759 |
760 | ```python
761 | """
762 | These tests cover DuckDuckGo searches.
763 | """
764 |
765 | from pages.result import DuckDuckGoResultPage
766 | from pages.search import DuckDuckGoSearchPage
767 |
768 |
769 | def test_basic_duckduckgo_search(browser):
770 | search_page = DuckDuckGoSearchPage(browser)
771 | result_page = DuckDuckGoResultPage(browser)
772 | PHRASE = "panda"
773 |
774 | # Given the DuckDuckGo home page is displayed
775 | search_page.load()
776 |
777 | # When the user searches for "panda"
778 | search_page.search(PHRASE)
779 |
780 | # Then the search result title contains "panda"
781 | assert PHRASE in result_page.title()
782 |
783 | # And the search result query is "panda"
784 | assert PHRASE == result_page.search_input_value()
785 |
786 | # And the search result links pertain to "panda"
787 | for title in result_page.result_link_titles():
788 | assert PHRASE.lower() in title.lower()
789 | ```
790 |
791 | Rerun the test using `pipenv run python -m pytest`.
792 | Now, finally, it should run to completion and pass!
793 | The test will take a few second to run because it must wait for page loads.
794 | Chrome should pop up and automatically go through all test steps.
795 | Try not to interfere with the browser as the test runs.
796 | Make sure pytest doesn't report any failures when it completes.
797 |
798 | ### Part 6: Configuring Multiple Browsers
799 |
800 | *Time Estimate: 15 Minutes*
801 |
802 | *Example Branch: example/6-browser-config*
803 |
804 | Our test currently runs on Chrome,
805 | but it should be able to run on other browsers, too.
806 | Any Web UI test should be configurable to run on any applicable browser.
807 | Let's run it on Headless Chrome and Firefox!
808 |
809 | Browser choice is an aspect of testing.
810 | In theory, every test should run on every supported browser.
811 | Thus, browser choice should be treated as an input for test automation.
812 | It should not be hard-coded into automation code.
813 | It should also not be written as pytest parameters.
814 | One test session should use one browser.
815 | If another browser needs to be tested, then launch another test session.
816 | This design keeps test code and test executions simpler.
817 |
818 | Create a new file named `config.json` in the project's root directory.
819 | JSON files are very easy to use in Python.
820 | The `json` module is part of the standard library,
821 | and JSON files can be parsed into dictionaries with one line.
822 | Add the following lines:
823 |
824 | ```json
825 | {
826 | "browser": "Chrome",
827 | "implicit_wait": 10
828 | }
829 | ```
830 |
831 | Notice how these inputs correspond to values in the `browser` fixture.
832 | Then, add a new fixture to `tests/conftest.py`:
833 |
834 | ```python
835 | import json
836 |
837 | @pytest.fixture
838 | def config(scope='session'):
839 |
840 | # Read the file
841 | with open('config.json') as config_file:
842 | config = json.load(config_file)
843 |
844 | # Assert values are acceptable
845 | assert config['browser'] in ['Firefox', 'Chrome', 'Headless Chrome']
846 | assert isinstance(config['implicit_wait'], int)
847 | assert config['implicit_wait'] > 0
848 |
849 | # Return config so it can be used
850 | return config
851 | ```
852 |
853 | This fixture reads the `config.json` file.
854 | It also validates the inputs so that tests won't run if the inputs are bad.
855 | The fixture's *scope* is set to "session" so that the fixture is called only one time for all tests.
856 | There is no need to read it repeatedly for every test.
857 |
858 | Update the `browser` fixture to use these inputs:
859 |
860 | ```python
861 | @pytest.fixture
862 | def browser(config):
863 |
864 | # Initialize the WebDriver instance
865 | if config['browser'] == 'Firefox':
866 | b = selenium.webdriver.Firefox()
867 | elif config['browser'] == 'Chrome':
868 | b = selenium.webdriver.Chrome()
869 | elif config['browser'] == 'Headless Chrome':
870 | opts = selenium.webdriver.ChromeOptions()
871 | opts.add_argument('headless')
872 | b = selenium.webdriver.Chrome(options=opts)
873 | else:
874 | raise Exception(f'Browser "{config["browser"]}" is not supported')
875 |
876 | # Make its calls wait for elements to appear
877 | b.implicitly_wait(config['implicit_wait'])
878 |
879 | # Return the WebDriver instance for the setup
880 | yield b
881 |
882 | # Quit the WebDriver instance for the cleanup
883 | b.quit()
884 | ```
885 |
886 | Fixtures can call fixtures.
887 | Here, `browser` calls `config` and then uses its parts to set the browser and implicit wait time.
888 | Notice that Headless Chrome just uses the Chrome WebDriver with extra arguments.
889 |
890 | Nothing else needs to be updated in order to change the browser.
891 | Run the test using `pipenv run python -m pytest` with Chrome to verify no harm was done.
892 | You should see the test run successfully.
893 |
894 | Then, change the config's *browser* to "Headless Chrome" and rerun the test.
895 | You won't see the browser window appear, but the test should still pass.
896 | Why? "Headless" mode won't render pages visibly.
897 | It's great for automated testing because it's slightly more efficient than "regular" Chrome.
898 |
899 | Finally, try "Firefox". Does it work? Warning: it may or may not! Oh no!
900 | Don't panic if it doesn't work. We'll fix it in the next part.
901 |
902 | ### Part 7: Handling Race Conditions
903 |
904 | *Time Estimate: 15 Minutes*
905 |
906 | *Example Branch: example/7-race-conditions*
907 |
908 | When running the search test using Firefox, you might hit the following failure:
909 |
910 | ```
911 | # Then the search result title contains "panda"
912 | > assert PHRASE in result_page.title()
913 | E AssertionError: assert 'panda' in 'DuckDuckGo — Privacy, simplified.'
914 | ```
915 |
916 | Or, the test might pass.
917 | Why would the test fail on Firefox if it passed for Chrome?
918 | And why is there a chance that it *might* fail or *might* pass?
919 | Let's revisit the test case steps:
920 |
921 | ```gherkin
922 | Scenario: Basic DuckDuckGo Search
923 | Given the DuckDuckGo home page is displayed
924 | When the user searches for “panda”
925 | Then the search result title contains “panda”
926 | And the search result query is “panda”
927 | And the search result links pertain to “panda”
928 | ```
929 |
930 | Step 2 performs the search.
931 | Then, step 3 checks the title of the page.
932 | Unfortunately, step 3 has a *race condition*.
933 | Remember, the browser and the automation are two separate processes.
934 | When the automation triggers the search, the browser will load the new page and title.
935 | At the same time, the automation will continue to execute the test.
936 | If the automation executes the assertion *before* the new page title loads,
937 | then the assertion will fail.
938 | Chrome was fast enough to avoid the race condition,
939 | but Firefox was slow enough to trigger it.
940 |
941 | Race conditions are the bane of Web UI testing.
942 | They can be difficult to predict when writing tests.
943 | They can also be difficult to identify in test results
944 | because they typically happen *intermittently*.
945 | Web UI tests gain a bad reputation for being "flaky"
946 | whenever race conditions are not handled appropriately.
947 |
948 | Automation must always wait for page components to be ready before interacting with them.
949 | [Implicit waits](https://selenium-python.readthedocs.io/waits.html#implicit-waits)
950 | work well for Web elements,
951 | but they don't work for browser attributes like page title.
952 | They are best for small projects.
953 | [Explicit waits](https://selenium-python.readthedocs.io/waits.html#explicit-waits)
954 | are more customizable, but they require more code.
955 | They are typically the better option for large projects that need different times and conditions.
956 | As a best practice, automation should use only one type of waiting.
957 | Mixing implicit and explicit waits can have unexpected consequences.
958 |
959 | Thankfully, there's a shortcut we can use to fix `test_basic_duckduckgo_search`.
960 | The other two assertions use implicit waits for other elements on the page.
961 | By the time those elements are loaded, the title would also be loaded.
962 | Therefore, we can move step 3 to the end of the scenario to be the last thing we check.
963 |
964 | The updated code for `tests/test_search.py` should be:
965 |
966 | ```python
967 | """
968 | These tests cover DuckDuckGo searches.
969 | """
970 |
971 | from pages.result import DuckDuckGoResultPage
972 | from pages.search import DuckDuckGoSearchPage
973 |
974 |
975 | def test_basic_duckduckgo_search(browser):
976 | search_page = DuckDuckGoSearchPage(browser)
977 | result_page = DuckDuckGoResultPage(browser)
978 | PHRASE = "panda"
979 |
980 | # Given the DuckDuckGo home page is displayed
981 | search_page.load()
982 |
983 | # When the user searches for "panda"
984 | search_page.search(PHRASE)
985 |
986 | # Then the search result query is "panda"
987 | assert PHRASE == result_page.search_input_value()
988 |
989 | # And the search result links pertain to "panda"
990 | for title in result_page.result_link_titles():
991 | assert PHRASE.lower() in title.lower()
992 |
993 | # And the search result title contains "panda"
994 | # (Putting this assertion last guarantees that the page title will be ready)
995 | assert PHRASE in result_page.title()
996 | ```
997 |
998 | Rerun the test using `pipenv run python -m pytest` with Firefox to verify the fix.
999 | Then, rerun it again with Chrome and Headless Chrome to make sure those browsers still work.
1000 |
1001 | Always watch out for race conditions,
1002 | always wait for things to be ready before interacting with them,
1003 | and always run tests multiple times across multiple configurations to identify problems.
1004 |
1005 | ### Part 8: Running Tests in Parallel
1006 |
1007 | *Time Estimate: 15 Minutes*
1008 |
1009 | *Example Branch: example/8-parallel-testing*
1010 |
1011 | Unfortunately, Web UI tests are very slow compared to unit tests and service API tests.
1012 | The best way to speed them up is to run them in parallel.
1013 |
1014 | First, let's parametrize `test_basic_duckduckgo_search` so that we have more than one test to run.
1015 | Any pytest test or fixture may be [parametrized](https://docs.pytest.org/en/latest/parametrize.html).
1016 | Update the code in `tests/test_search.py` to be:
1017 |
1018 | ```python
1019 | """
1020 | These tests cover DuckDuckGo searches.
1021 | """
1022 |
1023 | import pytest
1024 |
1025 | from pages.result import DuckDuckGoResultPage
1026 | from pages.search import DuckDuckGoSearchPage
1027 |
1028 |
1029 | @pytest.mark.parametrize('phrase', ['panda', 'python', 'polar bear'])
1030 | def test_basic_duckduckgo_search(browser, phrase):
1031 | search_page = DuckDuckGoSearchPage(browser)
1032 | result_page = DuckDuckGoResultPage(browser)
1033 |
1034 | # Given the DuckDuckGo home page is displayed
1035 | search_page.load()
1036 |
1037 | # When the user searches for the phrase
1038 | search_page.search(phrase)
1039 |
1040 | # Then the search result query is the phrase
1041 | assert phrase == result_page.search_input_value()
1042 |
1043 | # And the search result links pertain to the phrase
1044 | for title in result_page.result_link_titles():
1045 | assert phrase.lower() in title.lower()
1046 |
1047 | # And the search result title contains the phrase
1048 | # (Putting this assertion last guarantees that the page title will be ready)
1049 | assert phrase in result_page.title()
1050 | ```
1051 |
1052 | The test will now run three times with different search phrases.
1053 | Rerun the tests to make sure they all work.
1054 | You will notice that they run one at a time.
1055 |
1056 | Next, install [pytest-xdist](https://docs.pytest.org/en/3.0.1/xdist.html),
1057 | the pytest plugin for parallel testing:
1058 |
1059 | ```bash
1060 | $ pipenv install pytest-xdist
1061 | ```
1062 |
1063 | Finally, run the tests using the following command:
1064 |
1065 | ```bash
1066 | $ pipenv run python -m pytest -n 3
1067 | ```
1068 |
1069 | The "-n 3" arguments tells pytest to run 3 tests in parallel.
1070 | We have 3 example tests, and most machines can handle 3 Web UI tests simultaneously.
1071 | When the tests run, notice how 3 browser instances open at once - one per test.
1072 |
1073 | Run the tests a few times using Chrome and Firefox.
1074 | Look to see how long the tests typically take per browser.
1075 | Also, look to see if any intermittent failures happen.
1076 | Then, try using Headless Chrome.
1077 | Most likely, Headless Chrome will be significantly faster and more reliable
1078 | that regular Chrome and Firefox.
1079 |
1080 | As a warning, parallel testing can be dangerous.
1081 | Make sure that tests avoid *collisions*.
1082 | Collisions happen when tests simultaneously access shared state.
1083 | For example, one test could try to access a database record while another test deletes it.
1084 | Thankfully, our DuckDuckGo search tests do not have any collisions
1085 | because they make independent searches in separate browser instances.
1086 |
1087 | Whenever running tests in parallel,
1088 | carefully tune the number of threads to minimize the total test execution time.
1089 | More threads does *not* necessarily mean faster testing.
1090 | Too many parallel tests will choke system resources.
1091 |
1092 | Anecdotally, for Web UI tests:
1093 |
1094 | * 1 test per processor minimizes total execution time without slowing down individual tests
1095 | * 2 tests per processor minimizes total execution time further but slows down individual tests
1096 | * more than 2 tests per processor does not meaningfully shrink total execution time further
1097 | * memory size does not have a significant impact on total execution time
1098 |
1099 | One machine can scale up only so far.
1100 | For massive parallel testing, try using
1101 | [Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2).
1102 | Alternatively, many companies provide cloud-based solutions for parallel WebDriver testing.
1103 | Check the *Resources* section below for a list.
1104 |
1105 | To learn more about parallel testing in general, read
1106 | [To Infinity and Beyond: A Guide to Parallel Testing](https://automationpanda.com/2018/01/21/to-infinity-and-beyond-a-guide-to-parallel-testing/).
1107 |
1108 | Congrats! You have completed the guided part of this tutorial!
1109 |
1110 | ## Independent Exercises
1111 |
1112 | The guided tutorial covered one very basic search test, but DuckDuckGo has many more features.
1113 | Try to write some new tests for DuckDuckGo independently.
1114 | Here are some suggestions:
1115 |
1116 | * search for different phrases
1117 | * search by clicking the button instead of typing RETURN
1118 | * click a search result
1119 | * expand "More Results" at the bottom of the result page
1120 | * verify auto-complete suggestions pertain to the search text
1121 | * search by selecting an auto-complete suggestion
1122 | * search a new phrase from the results page
1123 | * do an image search
1124 | * do a video search
1125 | * do a news search
1126 | * change settings
1127 | * change region
1128 |
1129 | These tests will require new page objects, locators, and interaction methods.
1130 | See how many tests you can automate on your own!
1131 | If you get stuck, ask for help.
1132 |
1133 | ## Additional Resources
1134 |
1135 | This DjangoCon 2019 *Hands-On Web UI Testing* tutorial is related to other tutorials by Andrew Knight:
1136 |
1137 | * PyOhio 2019: [Hands-On Web UI Testing](https://github.com/AndyLPK247/pyohio-2019-web-ui-testing)
1138 | * TestProject: [Web Testing Made Easy with Python, Pytest and Selenium WebDriver](https://blog.testproject.io/2019/07/09/open-source-test-automation-python-pytest-selenium-webdriver/)
1139 | * SmartBear: [Hands-On UI Testing with Python](https://automationpanda.com/2019/08/19/hands-on-ui-testing-with-python-smartbear-webinar/)
1140 |
1141 | [Test Automation University](https://testautomationu.applitools.com/)
1142 | offers free online courses on several testing and automation topics.
1143 | All TAU courses are great, but the following ones compliment this tutorial especially well:
1144 |
1145 | * [Web Element Locator Strategies](https://testautomationu.applitools.com/web-element-locator-strategies/) shows how to write good locators and use Chrome DevTools.
1146 | * [Behavior-Driven Python with pytest-bdd](https://testautomationu.applitools.com/behavior-driven-python-with-pytest-bdd/) shows how to use `pytest-bdd` to write BDD-style tests.
1147 | * [Setting a Foundation for Successful Test Automation](https://testautomationu.applitools.com/setting-a-foundation-for-successful-test-automation/) shows how to run a testing project the right way.
1148 |
1149 | Many companies provide cloud-based solutions for parallel, multi-browser, multi-platform Web UI testing that work with WebDriver:
1150 |
1151 | * [BrowserStack](https://www.browserstack.com/)
1152 | * [LambdaTest](https://www.lambdatest.com/)
1153 | * [Sauce Labs](https://saucelabs.com/)
1154 | * [SmartBear CrossBrowserTesting](https://crossbrowsertesting.com/)
1155 | * [TestProject](https://testproject.io/)
1156 |
1157 | Other helpful links:
1158 |
1159 | * [AutomationPanda.com](https://automationpanda.com/)
1160 | * [Python](https://automationpanda.com/python/)
1161 | * [Testing](https://automationpanda.com/testing/)
1162 | * [Why Python is Great for Test Automation](https://automationpanda.com/2018/07/26/why-python-is-great-for-test-automation/)
1163 | * [Web Element Locators for Test Automation](https://automationpanda.com/2019/01/15/web-element-locators-for-test-automation/)
1164 | * [The Testing Pyramid](https://automationpanda.com/2018/08/01/the-testing-pyramid/)
1165 | * [To Infinity and Beyond: A Guide to Parallel Testing](https://automationpanda.com/2018/01/21/to-infinity-and-beyond-a-guide-to-parallel-testing/)
1166 | * [Selenium with Python](https://selenium-python.readthedocs.io/)
1167 | * [WebDriver API](https://selenium-python.readthedocs.io/api.html)
1168 | * [Waits](https://selenium-python.readthedocs.io/waits.html)
1169 | * [Locating Elements](https://selenium-python.readthedocs.io/locating-elements.html)
1170 | * [pytest.org](https://docs.pytest.org/)
1171 | * [Selenium Grid wiki](https://github.com/SeleniumHQ/selenium/wiki/Grid2)
1172 |
1173 | ## Special Thanks from Pandy
1174 |
1175 | Thank you to [DjangoCon 2019](https://2019.djangocon.us/) for inviting me to deliver this tutorial!
1176 |
1177 | Thank you to all the students who participated in this tutorial at DjangoCon 2019.
1178 |
1179 | Thank you to the following individuals who graciously reviewed this tutorial:
1180 |
1181 | * Michael Lynch ([@deliberatecoder](https://twitter.com/deliberatecoder))
1182 | * Satyank Tiwari ([@satyanktiwari](https://twitter.com/satyanktiwari))
1183 | * Adeel Mansoor ([@testadeel](https://twitter.com/testadeel))
1184 | * Rick Clymer ([@clymerrm](https://twitter.com/clymerrm))
1185 | * Katrina Durance ([@katdurance](https://twitter.com/katdurance))
1186 |
1187 | ## About the Author
1188 |
1189 | This tutorial was written and delivered by **Andrew Knight** (aka *Pandy*), the "Automation Panda".
1190 | Andy is a Pythonista who specializes in testing and automation.
1191 |
1192 | * Twitter: [@AutomationPanda](https://twitter.com/AutomationPanda)
1193 | * Blog: [AutomationPanda.com](https://automationpanda.com/)
1194 | * LinkedIn: [andrew-leland-knight](https://www.linkedin.com/in/andrew-leland-knight/)
1195 |
--------------------------------------------------------------------------------