{"id":21958947,"url":"https://github.com/bitcoder/automation_week_mot","last_synced_at":"2026-01-03T14:44:22.226Z","repository":{"id":146437993,"uuid":"305101397","full_name":"bitcoder/automation_week_mot","owner":"bitcoder","description":"Test automation code for Automation Week UI challenges of Ministry of Testing 2020","archived":false,"fork":false,"pushed_at":"2024-07-16T08:57:51.000Z","size":3667,"stargazers_count":0,"open_issues_count":6,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-01-23T22:41:53.405Z","etag":null,"topics":["modeling","python","test-automation","testing","testing-tools"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bitcoder.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-18T12:54:32.000Z","updated_at":"2020-10-29T14:38:51.000Z","dependencies_parsed_at":"2024-07-10T10:36:08.343Z","dependency_job_id":null,"html_url":"https://github.com/bitcoder/automation_week_mot","commit_stats":{"total_commits":12,"total_committers":2,"mean_commits":6.0,"dds":0.08333333333333337,"last_synced_commit":"162ae1b8dc5215e84374d82d4ca85aa6ffafc7cd"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitcoder%2Fautomation_week_mot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitcoder%2Fautomation_week_mot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitcoder%2Fautomation_week_mot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitcoder%2Fautomation_week_mot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bitcoder","download_url":"https://codeload.github.com/bitcoder/automation_week_mot/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244039241,"owners_count":20387835,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["modeling","python","test-automation","testing","testing-tools"],"created_at":"2024-11-29T09:17:58.236Z","updated_at":"2026-01-03T14:44:22.194Z","avatar_url":"https://github.com/bitcoder.png","language":"Python","readme":"# Automated tests for Automation Week UI challenges from MoT\n\nThis repo provides test automation code for Automation Week 2020 UI challenges (Ministry of Testing).\nDifferent implementations and techniques are provided, showcasing how this could be achieved. Each has pros/cons that have to be considered.\n\nNote: it's a learning exercise, so it may not be perfect. Nevertheless, I think it can be useful to get started.\n\n## Overview of UI challenges\n\nNext follows a description of the UI challenges for the sample website application [Restful Brooker Platform](https://automationintesting.online/) kindly provided by [Mark Winteringham](https://twitter.com/2bittester) / [Richard Bradshaw](https://twitter.com/FriendlyTester).\n\nThe tests are pretty simple and the purpose is to exercise test automation, refining the approach, be aware of some challenges, etc.\n\n![](images/target_sut.jpg)\n\n### Challenge 1: Beginner\n\n_Create an automated test that completes the contact us form on the homepage, submits it, and asserts that the form was completed successfully._\n\n### Challenge 2: Intermediate\n\n_Create an automated test that reads a message on the admin side of the site._\n\n_You’ll need to trigger a message in the first place, login as admin, open that specific message and validate its contents._\n\n### Challenge 3: Advanced\n\n_Create an automated test where a user successfully books a room from the homepage._\n\n_You’ll have to click ‘Book this Room’, drag over dates you wish to book, complete the required information and submit the booking._\n\n## Approach for implementing automated tests\n\nIn this repo, you'll find two different Python implementations for automated tests/checks:\n\n- one using pytest\n- another using Model-Based Testing (MBT), using AltWalker which in turn uses GraphWalker\n\nBoth make use of the Page Objects Model (POM) facilitated by the pypom library. As pages can have different sections/regions, we can abstract those precisely as classes inherited from the Region class. This will make code cleaner and more readable.\n\n```python\nclass FrontPage(Page):\n    \"\"\"Interact with frontpage.\"\"\"\n\n    _admin_panel_locator = (By.LINK_TEXT, \"Admin panel\")\n\n    class ContactForm(Region):\n\n        _contact_form_name_locator = (By.ID, \"name\")\n...\n```\n\nIn both implementations, you'll see references to a faking data library. I've combined controlled randomization of data to provide greater coverage; this is especially valuable in the MBT implementation as the model can be exercised automatically \"indefinitely\" (to a certain point).\n\n### Standard tests using pytest\n\nThis implementation is what I would call the traditional/common way of implementing automated tests.\nTests are implemented in the file [contact_form_pom_tests.py](contact_form_pom_tests.py), as seen in this example for the sucessful contact test.\n\n```python\n    def test_contact_form_successful(self):\n        page = FrontPage(self.driver, BASE_URL)\n        page.open()\n        page.contact_form.wait_for_region_to_load()\n        page.contact_form.fill_contact_data(name=\"sergio\", email=\"sergio.freire@example.com\", phone=\"+1234567890\",\n                                            subject=\"doubt\", description=\"Can I book rooms up to 2 months ahead of time?\")\n        self.assertEqual(page.contact_form.contact_feedback_message,\n                         f\"Thanks for getting in touch sergio!\\nWe'll get back to you about\\ndoubt\\nas soon as possible.\")\n```\n\nIn this case data was initially hard-coded. However, by using [faker](https://faker.readthedocs.io/en/master/) library we can create a [custom test data provider](tests/my_contact_provider.py) for the contact and our test method can be rewriten as:\n\n```python\n    def test_contact_form_successful(self):\n        page = FrontPage(self.driver, BASE_URL)\n        page.open()\n        page.contact_form.wait_for_region_to_load()\n        name = fake.valid_name()\n        email = fake.valid_email()\n        phone = fake.valid_phone()\n        subject = fake.valid_subject()\n        description = fake.valid_description()\n        page.contact_form.fill_contact_data(\n            name=name, email=email, phone=phone, subject=subject, description=description)\n        self.assertEqual(page.contact_form.contact_feedback_message,\n                         f\"Thanks for getting in touch {name}!\\nWe'll get back to you about\\n{subject}\\nas soon as possible.\")\n```\n\nThe first two challenges are solved in a similar way.\n\nHowever, for the last challenge there are validations for the information provided to the user in the calendar about the total nights and the total price amount.\nBesides checking the confirmation message displayed upon booking, also the persisted booking object is checked.\nThese checks are possible thanks to a basic implementation of a REST API client, that can obtain internal state of the stored data.\n\n\n```python\n   def test_book_successful(self):\n        page = FrontPage(self.driver, BASE_URL)\n        page.open()\n\n        # click on first room\n        room = page.rooms.available_rooms()[0]\n        page.rooms.click_book_room(room)\n\n        # book some nights, starting today\n        today = date.today()\n        start_date = today\n        total_nights = 2\n        end_date = today+timedelta(days=total_nights)\n        page.rooms.select_calendar_dates(\n            start_day=start_date.day, end_day=end_date.day)\n\n        # check displayed information on the total nights and price, before submitting\n        price_per_night = self.booker_api.get_rooms()[0]['roomPrice']\n        total_price = price_per_night*total_nights\n        page.rooms.select_calendar_dates(\n            start_day=start_date.day, end_day=end_date.day)\n        for selection_block in page.rooms.get_date_selection_blocks():\n            self.assertEqual(selection_block.text,\n                             f'{total_nights} night(s) - £{total_price}')\n\n        # submit booking request\n        start_date_str = start_date.isoformat()\n        end_date_str = end_date.isoformat()\n        first_name = fake.first_name()\n        last_name = fake.last_name()\n        email = fake.valid_email()\n        phone = fake.valid_phone()\n        page.rooms.fill_booking_contact_data(\n            first_name=first_name, last_name=last_name, email=email, phone=phone)\n        page.rooms.click_submit_booking()\n\n        # check confirmation message and stored booking on system\n        last_booking = self.booker_api.get_bookings()[-1]\n        self.assertEqual(page.rooms.booking_confirmed_message,\n                         f\"Booking Successful!\\nCongratulations! Your booking has been confirmed for:\\n{start_date_str} - {end_date_str}\\nClose\")\n        self.assertTrue(last_booking['firstname'] == first_name and last_booking['lastname'] == last_name and last_booking['bookingdates']['checkin']\n                        == start_date_str and last_booking['bookingdates']['checkout'] == end_date_str, f\"booking not found (last_booking={last_booking}\")\n```\n\n### Model-based tests using AltWalker and GraphWalker\n\nWith MBT, usually we have the model (either made using a visual model editor or from the IDE) and the underlying code.\nIn our case, models are stored in JSON format under the [models](models) directory.\nThe test code associated to vertices and egdges is implemented in the file [test.py](tests/test.py).\n\nUsing [Model Editor](https://altom.gitlab.io/altwalker/model-editor/) (or [GraphWalker Studio](https://graphwalker.github.io/)), we can model our application using a directed graph. In simple words, each vertex represents a state and each edge is a transition/action made in the application. Tests are made on the vertices/states.\nModeling is a challenge in itself and we can model the application and how we interact with it in different ways. Models are not exhaustive; they're a focused perspective on a certain behavior that we want to understand. MBT provides greater coverage and also a great way to visualize and discuss the application behavior/usage.\n\n### Addressing challenge 1 with MBT\n\nFor the first challenge (i.e. contact form submission), we start from an initial state, from where we just have one possible action/edge: load the frontpage.\nThen we can consider another state, where the frontpage is loaded and the contact form is available.\nTwo additional states are possible: one for a successful contact and another for an unsuccessful contact submission. We can go to these states by either submitting valid or invalid contact data.\nOne curious thing comes out from the model: after a successful contact, we can only make a new contact if we load/refresh the frontpage again. Was this an expected behavior? Well, we would have to discuss with the team.\n\n![](images/mbt_contact_form.jpg)\n\nWith GraphWalker Studio we can run the model in offline and see the paths (sequence of vertices and edges) performed.\n\nThe code for each vertex and edge is quite simple as seen ahead.\n\nExample of __e_submit_valid_contact_data__ code, showcasing usage of faker library:\n```python\n\n    def e_submit_valid_contact_data(self, data):\n        page = FrontPage(self.driver, BASE_URL)\n        page.contact_form.wait_for_region_to_load()\n\n        name = fake.valid_name()\n        email = fake.valid_email()\n        phone = fake.valid_phone()\n        subject = fake.valid_subject()\n        description = fake.valid_description()\n\n        data['global.last_contact_name'] = name\n        data['global.last_contact_email'] = email\n        data['global.last_contact_phone'] = phone\n        data['global.last_contact_subject'] = subject\n        data['global.last_contact_description'] = description\n\n        page.contact_form.fill_contact_data(\n            name=name, email=email, phone=phone, subject=subject, description=description)\n```\n\nExample of __v_contact_successful__ code:\n```python\n    def v_contact_successful(self, data):\n        page = FrontPage(self.driver, BASE_URL)        \n        name = data['last_contact_name']\n        subject = data['last_contact_subject']\n        self.assertEqual(page.contact_form.contact_feedback_message,\n                         f\"Thanks for getting in touch {name}!\\nWe'll get back to you about\\n{subject}\\nas soon as possible.\")\n```\n\n\n![](images/mbt_contact_form_offline.gif)\n\nOne can make this model a bit more detailed and complex, by making explicit edges/transitions for the process of submitting one field as invalid. This makes the graph harder to read though and it will only be relevant if we want to distinguish those cases.\n\n![](images/mbt_contact_form_detailed.jpg)\n\n### Addressing challenge 2 with MBT\n\nIn order to validate if the contact/message appears correctly in the admin page (2nd challenge), we start from a vertex/state related to a successful contact. This vertex has a shared state with the first model shared earlier, which allows AltWalker/GraphWalker to jump from one to the other.\n\nWe can then go to the admin panel, authenticate if needed, go to the inbox/messages section, open and check the details of the last contact message.\nWe can see several edges corresponding to actions that can be done, allowing us to transverse the graph and thus go to different application states.\n\nSome edges (e.g. e_admin_correct_login) have \"actions\" defined in the model, to set an internal variable that can be useful later on.\n\nExample:\n```javacript\nlogged_in=true;\n```\n\nSome edges (e.g. **e_click_admin_panel**, from **v_contact_successful** to **v_admin_login**) have \"guards\", so they're only performed if those guard conditions are true.\n\nExample:\n```javacript\nlogged_in!=true\n```\n\nIn this exercise, we take advantage of using model variables (e.g. last_contact_name, last_contact_subject) to temporarily store information about the last contact. The actual contact data used is implemented in code side and is populated on the model variables used for this purpose.\n\n![](images/mbt_message_backoffice.jpg)\n\n### Addressing challenge 3 with MBT\n\nChallenge 3 (i.e. new booking) can also be addressed using a simple model, having a variable named *total_nights*, defined at model level, for controlling the intended number of nights to book.\n\n![](images/mbt_new_booking1.jpg)\n\nThe previous model depicts a sequential set of actions and corresponding states, so it mimics a typical automated test as seen in the pytest implementation.\nEven though feasible, and as there's only one path in the graph, this model doesn't provide exceptional value except that it turns visible our own model of the system.\n\n_Note: another possible model could deal with the fact that the contact and date selection don't need to happen in sequence, and also provide the ability to jump back to the initial page. Well, many variations can be done depending on what we want to verify and the risks we have in mind._\n\n![](images/mbt_new_booking2.jpg)\n\n### Pre-requisites\n\n- Python3\n- Firefox (or other browser)\n- GraphWalker v4.2.0 (v4.3.0 has some issues with AltWalker v0.2.7); see instructions [here](https://graphwalker.github.io/)\n\nInstall the python dependencies:\n\n```pip install -r requirements.txt```\n\n### Running automated tests\n\n#### Standard tests using pytest\n\nIn order to run the standard pytest tests, just execute:\n```pytest -s contact_form_pom_tests.py.py```\n\nor, if you prefer using the helper bash script:\n\n```./run_pytest.sh```\n\n#### Model-based tests using AltWalker and GraphWalker\n\nIn order to run AltWalker tests (e.g. for the contact form) you need to define the [path generator and stop condition(s)](https://github.com/GraphWalker/graphwalker-project/wiki/Generators-and-stop-conditions). For example,\n\n_random(vertex_coverage(100) and edge_coverage(100))_\n\n\n... will use the \"random\" generator and will stop walking in the graph if all vertices and edges have been visited.\nIf we want, we can add additional conditions. For example, to keep running the tests until a certain amount of time has elapsed.\n\n_random(vertex_coverage(100) and edge_coverage(100) and time_duration())_\n\n\nTo run the tests,and perform some initial consistency validations, just execute the following commands.\n\n```bash\n\naltwalker check -m models/contact_form.json \"random(vertex_coverage(100) and edge_coverage(100))\"\naltwalker verify -m models/contact_form.json tests\naltwalker online tests -m models/contact_form.json \"random(vertex_coverage(100) and edge_coverage(100))\"\n```\n\nIf you prefer, you may also use the helper bash scripts:\n\n```./run_altwalker_contact.sh```\n\n```./run_altwalker_contact_detailed.sh```\n\n```./run_altwalker_contact_with_message.sh```\n\n```./run_altwalker_new_booking1.sh```\n\nIf you wish to run the tests against a specific URL instead of the default (https://aw1.automationintesting.online), you just need to define the BASE_URL environment variable.\n\n```bash\nBASE_URL=\"https://aw3.automationintesting.online\" ./run_pytest.sh\n```\n\n## Configuration\n\nThe default configuration parameters are defined in [config.ini](config.ini). Some may be overridden by environment variables, if they exist.\n\n## Final thoughts\n\nOn the app:\n\n- app has some testability issues that inhibit usage of stable locators\n- app has several bugs, intended or not :)\n\nOn the automated tests implementation:\n\n- we can automate checks for the current behavior but we can't guarantee if that is the original intended one (e.g. phone format, fields size, flow behavior)\n- randomization of data: control it, for reproduction purposes, using a seed\n- Model-Based Testing:\n  - can easily expose usage scenarios that go beyond happy paths\n  - models can connect with one another through shared states, which is useful\n  - can be combined with randomization of data to provide greater coverage\n  - beware of managing data outside of the model (i.e. in the code) and in the model using variables\n  - not used to replace traditional tests (happy path and negative tests)\n  - can be harder to debug\n\n## Room for improvement\n\nThese are some points that can be further improved, namely locators.\nOthers could include:\n\n- ability to define browser (e.g. firefox, chrome, etc) to use\n- ability to configure headless behavior\n\n## References\n\n- [AltWalker documentation](https://altom.gitlab.io/altwalker/altwalker/index.html)\n- [Examples provided by AltWalker](https://altom.gitlab.io/altwalker/altwalker/examples.html)\n- [GraphWalker and GraphWalker Studio](https://graphwalker.github.io/)\n- Python packages\n  - [pypom](https://pypom.readthedocs.io/en/latest/)\n  - [webdriver](https://pypi.org/project/selenium/)\n  - [faker](https://faker.readthedocs.io/en/master/)\n\n## Contact\n\nYou can find me on [Twitter](https://twitter.com/darktelecom).\n\n## LICENSE\n\n[MIT](LICENSE).","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitcoder%2Fautomation_week_mot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbitcoder%2Fautomation_week_mot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitcoder%2Fautomation_week_mot/lists"}