diff --git a/README.md b/README.md index 9547591..97669a9 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ ## About me -Results-driven QA Engineer skilled in executing comprehensive test cases, API testing, and developing automated tests using Python. Known for effective collaboration with cross-functional teams to enhance application quality and user experience. -Looking to transition into new field with background shaped by rigorous quality assurance and control processes. Skilled in identifying inefficiencies, solving complex problems, and implementing effective solutions in structured environment. Seeking to bring these transferrable skills to dynamic and innovative role. +Results-driven QA Engineer specializing in manual testing, API testing, and test automation using Python, pytest, and Playwright. Experienced in designing test scenarios, investigating defects, validating complex business logic, and building automated E2E test coverage. + +My technical background includes backend testing, SQL, API testing, accessibility testing, CI/CD, Linux, Docker, and automated testing. I focus on combining strong test design with technical investigation to identify risks and improve product quality. ## Contact @@ -26,10 +27,11 @@ You can find examples of the described skills in the [Examples of my work](#exam __Testing__ -- can perform manual functional testing, regression testing, smoke testing, sanity testing, integration testing, visual/UI testing, API testing, accessibility testing, code review -- can code autotests using playwright and pytest, with Allure for test reporting (e2e, unit, integration) +- can perform manual functional testing, regression testing, smoke testing, sanity testing, integration testing, visual/UI testing, API testing, accessibility testing and code review +- can develop automated tests using Playwright and pytest, with Allure for test reporting +- experienced with E2E, unit and integration testing - can verify fixes through regression testing and track defect resolution status -- gained hands-on experience by testing applications in my carreer path and my projects +- gained hands-on experience testing applications in professional and personal projects __Mobile & cross-platform testing__ @@ -39,14 +41,20 @@ __Mobile & cross-platform testing__ __Test analysis & design__ -- familiar with functional decomposition of products, creating state transition diagrams, writing use cases -- can apply equivalence partitioning, boundary value analysis and methods of pairwise testing to generate test input data +- familiar with functional decomposition of products, creating state transition diagrams and writing use cases +- can apply equivalence partitioning, boundary value analysis and pairwise testing to generate test input data __API testing__ -- know the difference between SOAP, REST APIs, GraphQL, gRPC, WebSockets -- gained experience through manual and auto testing of APIs -- know how to manipulate requests and create test cases using the SoapUI tool and Postman tool +- understand the differences between SOAP, REST, GraphQL, gRPC and WebSockets +- gained practical experience through manual and automated API testing +- can manipulate requests and create test cases using SoapUI and Postman + +__Accessibility testing__ + +- experienced in automated accessibility testing using Axe / axe-core +- can validate accessibility of different application states, including forms, validation/error states, dynamic content and themes +- able to integrate accessibility checks into E2E test scenarios __Exploratory testing__ @@ -55,28 +63,30 @@ __Exploratory testing__ __Test documentation__ - can create test cases and checklists and know how and in which situations to use them effectively -- able to prepare comprehensive bug reports and provide detailed session reports and communicate with developing team to solve them +- able to prepare comprehensive bug reports and provide detailed session reports +- experienced in communicating defects and test results with development teams __SQL databases__ -- can write complex requests using subqueries -- familiar with the use of aliases and wildcards -- know the difference between joins and unions and can use them in queries +- can write complex queries using subqueries +- familiar with aliases and wildcards +- understand the difference between joins and unions and can use them in queries - can use ORM and different DBMS __Python__ -- can build a fullstack application using different technologies, such as ORM, DB, ASGI, WSGI, Validators and 90%+ test coverage -- can write a bunch of autotests for the application +- can build a full-stack application using technologies such as ORM, DB, ASGI, WSGI and validators +- can develop automated tests for applications using pytest +- experienced with unit, integration and E2E test automation __Network__ -- know how networks work +- understand basic networking concepts - experienced with Cisco Packet Tracer for network simulation and configuration __CI/CD & DevOps__ -- familiar with CI/CD process and its role in the software delivery pipeline +- familiar with CI/CD processes and their role in the software delivery pipeline - can use Ansible for configuration management and automation - comfortable working in Linux/Unix environments @@ -84,7 +94,7 @@ __CI/CD & DevOps__ __TestRail__ -- know how to create test cases and test suits +- know how to create test cases and test suites - able to run created test cases - know how to use dashboards and statistics @@ -98,21 +108,26 @@ __API testing tools__ - Postman - Insomnia - SoapUI -- HTTPToolkit +- HTTP Toolkit - Proxyman -__Test reporting__ +__Test automation__ +- Playwright +- pytest - Allure -- Playwright reports + +__Accessibility testing__ + +- Axe / axe-core __Browser developer tools__ -- know how to test the application using console, inspector, debugging, network and storage sections +- know how to test applications using console, inspector, debugger, network and storage sections __Virtualization software__ -- able to set up and operate virtual machines in different software like Virtualbox, qemu, vmware, hyperv, docker and podman +- able to set up and operate virtual machines using VirtualBox, QEMU, VMware, Hyper-V, Docker and Podman __Ansible__ @@ -136,24 +151,31 @@ The project demonstrates a complete CI/CD pipeline: Gitea Actions builds a Docke __[The DisExcel Project](https://git.homyk.space/MH.Dmitrii/The_DisExcel_project)__ -My most advanced and up-to-date project — a FastAPI backend with async SQLAlchemy 2.0, JWT authentication with refresh token rotation and revocation, and a permission/group-based RBAC system. Includes layered architecture (web → service → database → models), Alembic migrations, and a full test suite (unit, integrated, e2e) with pytest, pytest-asyncio and Allure reporting. Also includes Docker and Ansible(planning) configs for deployment. +My most advanced and up-to-date project — a FastAPI backend with async SQLAlchemy 2.0, JWT authentication with refresh token rotation and revocation, and a permission/group-based RBAC system. Includes layered architecture (web → service → database → models), Alembic migrations, and a full test suite (unit, integration, E2E) with pytest, pytest-asyncio and Allure reporting. Also includes Docker and Ansible configurations for deployment. __[Auth Project](https://git.homyk.space/MH.Dmitrii/auth-project)__ -A basic FastAPI authentication and authorization service, built without relying on the framework's built-in auth mechanisms. Implements JWT-based login, bcrypt password hashing, and a flat permission model with privilege escalation protection. A focused example of building auth logic from scratch and understanding it deeply enough to test it effectively. -using console, inspector, debugging, network and storage sections +A basic FastAPI authentication and authorization service, built without relying on the framework's built-in auth mechanisms. Implements JWT-based login, bcrypt password hashing, and a flat permission model with privilege escalation protection. A focused example of building authentication logic from scratch and understanding it deeply enough to test it effectively. __[Nova Registration Form — QA Test Portfolio](https://git.homyk.space/MH.Dmitrii/QA-portfolio/src/branch/main/app)__ -A deliberately buggy registration form (HTML/CSS/JS, no backend) built as a self-contained manual testing exercise. Includes a full test plan, test cases covering boundary value analysis, equivalence partitioning and pairwise testing, and detailed bug reports (with screenshots) for 7 seeded defects. Available in both English and Russian. Demonstrates end-to-end manual QA workflow: test planning → test design → execution → defect reporting. +A deliberately buggy registration form (HTML/CSS/JS, no backend) built as a self-contained manual testing exercise. Includes a full test plan, test cases covering boundary value analysis, equivalence partitioning and pairwise testing, and detailed bug reports (with screenshots) for 7 seeded defects. Available in both English and Russian. Demonstrates an end-to-end manual QA workflow: test planning → test design → execution → defect reporting. + +__[Playwright E2E Testing — Anonymized Case Study](https://git.homyk.space/MH.Dmitrii/QA-portfolio/src/branch/main/automation/playwright)__ + +An anonymized excerpt from an enterprise E2E automation project. The original project is proprietary, so company-specific data, credentials, URLs and internal implementation details have been removed. + +The case study demonstrates Playwright and pytest automation using the Page Object Model, parametrized positive and negative scenarios, Allure reporting, password recovery flows, asynchronous email verification, dynamic security-question validation and cooldown/retry handling. + +It also includes automated accessibility testing using Axe / axe-core across multiple application states, including the login page, password recovery form, security questions, validation/error states and dark theme. ## Domain experience - __Healthcare SaaS__ — tested billing modules and report templates for medical staff - __Banking / Fintech__ — tested mobile banking applications on iOS and Android -- __Search engines__ - tested yandex browser, yandex maps, yandex games +- __Search engines__ — tested Yandex Browser, Yandex Maps and Yandex Games ## Courses and Certifications -QA manual, pasv - June 2023
-Interactive SQL, stepik - April 2023
+QA Manual, PASV — June 2023
+Interactive SQL, Stepik — April 2023
\ No newline at end of file diff --git a/automation/playwright/login_page_axe.py b/automation/playwright/login_page_axe.py new file mode 100644 index 0000000..7d8fc04 --- /dev/null +++ b/automation/playwright/login_page_axe.py @@ -0,0 +1,111 @@ +import pytest +import allure +from playwright.sync_api import Page, expect + + +@pytest.mark.login_page +class TestLoginPageAccessibility: + """Anonymized accessibility checks from an enterprise Playwright project.""" + + def _check_accessibility(self, login_page_devices, accessibility_checker): + login_page_devices.wait_for_load_state("networkidle") + accessibility_checker.check(login_page_devices) + + @pytest.mark.axe + def test_accessibility_login_page( + self, + login_page_devices: Page, + accessibility_checker, + ): + with allure.step("Check login page accessibility"): + self._check_accessibility(login_page_devices, accessibility_checker) + + @pytest.mark.axe + def test_accessibility_password_recovery( + self, + login_page_devices: Page, + login_page_object, + accessibility_checker, + ): + with allure.step("Open password recovery form"): + login_page_object.forgot_password_link.click() + + with allure.step("Check password recovery accessibility"): + self._check_accessibility(login_page_devices, accessibility_checker) + + @pytest.mark.axe + def test_accessibility_security_questions( + self, + login_page_devices: Page, + login_page_object, + security_questions_user, + accessibility_checker, + ): + with allure.step("Open password recovery form"): + login_page_object.forgot_password_link.click() + + with allure.step("Fill login and email"): + login_page_object.login_field.fill(security_questions_user.login) + login_page_object.email_field.fill(security_questions_user.email) + + with allure.step("Submit password recovery request"): + login_page_object.next_btn.click() + + with allure.step("Check security questions accessibility"): + self._check_accessibility(login_page_devices, accessibility_checker) + + @pytest.mark.axe + @pytest.mark.parametrize( + "username,password,expected_error", + [ + pytest.param( + "valid_user", + "invalid_password", + "Unable to logon with specified login and password", + id="invalid-password", + ), + pytest.param( + "", + "valid_password", + "Form validation failed", + id="empty-username", + ), + ], + ) + def test_accessibility_login_error_state( + self, + login_page_devices: Page, + login_page_object, + accessibility_checker, + username: str, + password: str, + expected_error: str, + ): + with allure.step("Fill login and password"): + login_page_object.login_field.fill(username) + login_page_object.password_field.fill(password) + + with allure.step("Submit login form"): + login_page_object.logon_btn.click() + + with allure.step("Check error message"): + expect(login_page_devices.get_by_text(expected_error)).to_contain_text( + expected_error + ) + + with allure.step("Check error state accessibility"): + self._check_accessibility(login_page_devices, accessibility_checker) + + @pytest.mark.axe + def test_accessibility_dark_theme( + self, + login_page_devices: Page, + accessibility_checker, + ): + with allure.step("Switch to dark theme"): + login_page_devices.locator("html").evaluate( + "element => element.className = 'dark'" + ) + + with allure.step("Check dark theme accessibility"): + self._check_accessibility(login_page_devices, accessibility_checker) \ No newline at end of file diff --git a/automation/playwright/login_page_tests.py b/automation/playwright/login_page_tests.py new file mode 100644 index 0000000..f1f8f6c --- /dev/null +++ b/automation/playwright/login_page_tests.py @@ -0,0 +1,310 @@ +import re + +import allure +import pytest +from playwright.sync_api import Page, expect + + +@pytest.mark.login_page +class TestLoginPage: + """Anonymized E2E examples from an enterprise Playwright project.""" + + @pytest.mark.e2e + @pytest.mark.parametrize( + "username,password,expected_error", + [ + pytest.param( + "valid_user", + "invalid_password", + "Unable to logon with specified login and password", + id="invalid-password", + ), + pytest.param( + "unknown_user", + "valid_password", + "Unable to logon with specified login and password", + id="unknown-user", + ), + pytest.param( + "", + "valid_password", + "Form validation failed", + id="empty-username", + ), + pytest.param( + "valid_user", + "", + "Form validation failed", + id="empty-password", + ), + pytest.param( + r"!@#$%^&*()_+QWERTYUIOP{}ASDFGHJKL:\"|~ZXCVBNM<>?±§ ", + r"!@#$%^&*()_+QWERTYUIOP{}ASDFGHJKL:\"|~ZXCVBNM<>?±§ ", + "Unable to logon with specified login and password", + id="special-characters", + ), + pytest.param( + "qwertyuiopasdfghjklzxcvbnm" * 2, + "qwertyuiopasdfghjklzxcvbnm" * 2, + "Form validation failed", + id="overlong-input", + ), + pytest.param( + "non_latin_user", + "non_latin_password", + "Form validation failed", + id="non-latin-input", + ), + ], + ) + def test_login_negative( + self, + login_page: Page, + login_page_object, + username: str, + password: str, + expected_error: str, + ): + with allure.step("Fill login and password"): + login_page_object.login_field.fill(username) + login_page_object.password_field.fill(password) + + with allure.step("Click Logon button"): + login_page_object.logon_btn.click() + + with allure.step("Check error message"): + expect(login_page.get_by_text(expected_error)).to_contain_text(expected_error) + + @pytest.mark.e2e + def test_login_positive(self, login_page: Page, login_page_object, test_user): + with allure.step("Fill valid credentials"): + login_page_object.login_field.fill(test_user.login) + login_page_object.password_field.fill(test_user.password) + + with allure.step("Click Logon button"): + login_page_object.logon_btn.click() + + with allure.step("Check successful login"): + expect(login_page).not_to_have_url(login_page.url) + + @pytest.mark.e2e + def test_password_reset_positive( + self, + login_page: Page, + login_page_object, + password_reset_user, + mail_checker, + ): + with allure.step("Open password reset form"): + login_page_object.forgot_password_link.click() + + with allure.step("Close password reset form"): + login_page_object.close_btn.click() + expect(login_page_object.reset_pass_txt).to_be_hidden() + + with allure.step("Reopen password reset form"): + login_page_object.forgot_password_link.click() + + with allure.step("Fill login and email"): + login_page_object.login_field.fill(password_reset_user.login) + login_page_object.email_field.fill(password_reset_user.email) + + with allure.step("Submit password reset request"): + login_page_object.next_btn.click() + + with allure.step("Check reset email"): + mail = mail_checker.wait_for_email(positive=True, timeout=60) + print("Received email:", mail["subject"]) + + with allure.step("Check confirmation message"): + expect(login_page).to_contain_text( + "Password reset confirmation message" + ) + + @pytest.mark.e2e + @pytest.mark.parametrize( + "login,email,expected_result", + [ + pytest.param("test_user", " ", "Form validation failed", id="empty-email"), + pytest.param("test_user", "@", "Form validation failed", id="invalid-email-1"), + pytest.param("test_user", "test@", "Form validation failed", id="invalid-email-2"), + pytest.param( + "test_user", + "test@test.", + "Form validation failed", + id="invalid-email-3", + ), + pytest.param( + "test_user", + ".test@test.com", + "Form validation failed", + id="invalid-email-4", + ), + pytest.param( + "test_user", + "te..st@test.com", + "Form validation failed", + id="invalid-email-5", + ), + pytest.param( + "test_user", + "non_latin@test.com", + "Form validation failed", + id="non-latin-email", + ), + pytest.param( + "test_user", + "test@test.com", + "Reset password request was accepted", + id="unknown-email-for-valid-user", + ), + pytest.param( + "unknown_user", + "test@test.com", + "Reset password request was accepted", + id="unknown-user", + ), + pytest.param( + "", + "test@test.com", + "Form validation failed", + id="empty-login", + ), + pytest.param( + "test_user", + "test@test", + "Reset password request was accepted", + id="email-without-tld", + ), + pytest.param( + "test_user", + "!@test", + "Reset password request was accepted", + id="non-standard-email-format", + ), + ], + ) + def test_password_reset_negative( + self, + login_page: Page, + login_page_object, + mail_checker, + login: str, + email: str, + expected_result: str, + ): + with allure.step("Open password reset form"): + login_page_object.forgot_password_link.click() + + with allure.step("Fill login and email"): + login_page_object.login_field.fill(login) + login_page_object.email_field.fill(email) + + if expected_result == "Form validation failed": + with allure.step("Verify submit button is disabled"): + expect(login_page_object.next_btn).to_be_disabled() + else: + with allure.step("Submit password reset request"): + login_page_object.next_btn.click() + + with allure.step("Check application response"): + expect(login_page).to_contain_text(expected_result) + + with allure.step("Check that no reset email is sent"): + mail_checker.wait_for_email(positive=False, timeout=20) + + @pytest.mark.e2e + def test_password_reset_security_questions_positive( + self, + login_page: Page, + login_page_object, + security_questions_user, + mail_checker, + ): + with allure.step("Open password reset form"): + login_page_object.forgot_password_link.click() + + with allure.step("Fill login and email"): + login_page_object.login_field.fill(security_questions_user.login) + login_page_object.email_field.fill(security_questions_user.email) + + with allure.step("Submit password reset request"): + login_page_object.next_btn.click() + + answers = [] + expected_questions = { + "question_type_a": "answer_a", + "question_type_b": "answer_b", + "question_type_c": "answer_c", + "question_type_d": "answer_d", + } + + with allure.step("Answer security questions"): + for index in range(1, 5): + question_text = login_page.get_by_text( + re.compile(fr"Question {index}") + ).inner_text() + + current_answer = None + question_lower = question_text.lower() + + for question_type, answer in expected_questions.items(): + if question_type in question_lower: + current_answer = answer + break + + assert current_answer is not None, ( + f"Unknown security question: {question_text}" + ) + + answers.append(current_answer) + assert len(answers) == len(set(answers)), ( + f"Duplicate security question detected: {question_text}" + ) + + login_page_object.answer_field.fill(current_answer) + login_page_object.next_btn.nth(1).click() + + with allure.step("Check password reset email"): + mail = mail_checker.wait_for_email(positive=True, timeout=60) + print("Received email:", mail["subject"]) + + with allure.step("Check successful password reset"): + expect(login_page).to_contain_text("Password reset completed") + + @pytest.mark.e2e + def test_password_reset_security_questions_negative( + self, + login_page: Page, + login_page_object, + security_questions_user, + ): + with allure.step("Open password reset form"): + login_page_object.forgot_password_link.click() + + with allure.step("Fill login and email"): + login_page_object.login_field.fill(security_questions_user.login) + login_page_object.email_field.fill(security_questions_user.email) + + with allure.step("Submit password reset request"): + login_page_object.next_btn.click() + + with allure.step("Submit invalid answers"): + for _ in range(3): + login_page_object.answer_field.fill("invalid_answer") + login_page_object.next_btn.nth(1).click() + + expect(login_page).to_contain_text( + "Incorrect answer. Try again" + ) + login_page_object.ok_btn.click() + + with allure.step("Check cooldown timer"): + expect(login_page_object.sixty_btn).to_be_disabled() + expect(login_page_object.sixty_btn).to_contain_text(re.compile(r"\d+")) + + with allure.step("Wait for cooldown to finish"): + expect(login_page_object.next_btn.nth(1)).to_be_enabled(timeout=61_000) + expect(login_page_object.next_btn.nth(1)).not_to_contain_text( + re.compile(r"\d+") + ) \ No newline at end of file diff --git a/automation/playwright/readme.md b/automation/playwright/readme.md new file mode 100644 index 0000000..7840b19 --- /dev/null +++ b/automation/playwright/readme.md @@ -0,0 +1,71 @@ +Playwright E2E Testing — Anonymized Case Study + +This is an anonymized excerpt from an enterprise E2E automation project. + +The original project is proprietary and cannot be published. +The code has been sanitized to remove company-specific data, credentials, URLs and internal implementation details. + +Stack + +* Python +* pytest +* Playwright +* Allure +* Axe / axe-core + +Covered scenarios + +Authentication + +* valid credentials +* invalid username +* invalid password +* empty fields +* invalid characters +* boundary/long input +* error state validation + +Password recovery + +* valid login/email +* invalid email +* unknown user +* form validation +* email notification +* security questions +* cooldown/retry behavior + +Accessibility Testing + +Accessibility checks were automated using Axe / axe-core as part of the E2E test suite. + +Covered UI states include: + +* login page +* password recovery form +* security questions +* validation/error states +* dark theme + +The accessibility checks are executed against different application states rather than only the initial page, helping detect accessibility regressions introduced by dynamic UI changes. + +Security questions + +* unique questions validation +* valid answers +* invalid answers +* duplicate question detection +* retry/cooldown timer + +Testing approach + +* Page Object Model +* parametrized tests +* positive/negative testing +* UI validation +* accessibility testing with Axe +* asynchronous email verification +* business-rule validation +* explicit assertions +* dynamic UI state validation +* E2E validation across multiple application components \ No newline at end of file