Duong Minh Ngoc (you can call me Tom)

Senior QA Automation Engineer, 5+ years in testing

LinkedIn profile

I automated 300+ regression test cases per retailer for 8 US grocery retailers, on iOS, Android and web. Regression testing for one retailer went from 5 to 6 testers to 2 to 3.

I built an Appium mobile test framework for iOS and Android, and I maintained and extended a Selenium web test framework. Around that core I test APIs, performance, security, SEO and accessibility, work with AI coding agents every day, and mentor new engineers. I am looking for a Senior QA Automation role, and my direction is QA Lead.

Where
Ho Chi Minh City, Vietnam (UTC+7). Open to on-site and hybrid roles here.
When
Available now
Relocation
Preferred, not required. I am a Vietnamese citizen and would need visa sponsorship.
Remote
Yes, in any time zone. No sponsorship needed.
Role type
Open to permanent or contract roles
English
IELTS Academic 7.0 (CEFR C1). Almost 3 years working directly with US and Canadian colleagues.
Degree
BSc in Software Engineering
Core skills
Python, pytest, Selenium, Appium, Postman and REST APIs, CI/CD on Bamboo, Allure, k6, SQL, Claude Code

There is no CV download here. Email me about your role and I will reply as soon as I can with a CV written for it.

Experience

Mercatus Technologies Inc

Jan 2024 to Oct 2026

Senior QA Automation Engineer, Mar 2026 to Oct 2026. Pre-Senior QA Automation Engineer, Mar 2024 to Mar 2026, promoted after probation. QC Engineer (probation), Jan to Mar 2024.

Grocery e-commerce platform, developed in an Agile environment. Web test suites for Southeastern Grocers (Winn-Dixie, Harveys), Stater Bros. Markets, Weis Markets, WinCo Foods, Buehler's and Piggly Wiggly. Mobile test suites for Stater Bros. Markets, Weis Markets, Big Y, Buehler's and Vallarta Supermarkets.

FPT Software Ho Chi Minh

Aug 2023 to Dec 2023

Tester. Outsourcing project that modernised an application for a Japanese printing company. Full testing cycle to the client's standards.

Hung Thai Technology Company

Jul 2021 to Jul 2023

QC Engineer. Web and mobile apps for residents of Phu My Hung: bill payments through the MoMo e-wallet and Payoo card payments, and a coupon loyalty system. Agile team.

Education and courses

BSc in Software Engineering, Ton Duc Thang University, 2018 to 2022.

Full Stack Open (University of Helsinki). Google Project Management and Google Data Analytics (Coursera). Selenium with Java and Cucumber (Udemy).

Languages

English, IELTS Academic 7.0 (CEFR C1). Vietnamese, native.

Test automation for iOS, Android and web

Result. 300+ regression test cases automated per retailer. While a retailer's suite was being built, a regression round needed 5 to 6 testers. Once the suite was ready it needed 2 to 3, and they spend their time on what automation should not cover: third-party integrations such as social sign-in, and visual checks.

Open the full screenshot: An IDE, a terminal with a table of test results, and an iPhone simulator showing a grocery app.
A mobile test session. The table on the left is the run summary: one test passed and three failed on two app defects, each traced to its ticket.

At Mercatus Technologies I tested a grocery e-commerce platform used by 8 US retailers. The platform is white-label: a base product holds every feature, and each retailer gets its own version with the look and the features it asked for.

I inherited the Selenium web framework with part of the tests for the base product. I maintained it, extended it as the base product gained features, and wrote the tests for the retailers' versions, on Chrome, Firefox and Safari. When my work moved to mobile, I built the Appium framework on top of it, for iOS and Android. The apps are hybrid, so the tests switch between the webview and native screens.

I wrote the automated tests for the retailers' versions myself, on web and on mobile. They are end-to-end (E2E) tests, and each one follows a test case written by me or by a manual tester.

Tools. Python, pytest, pytest-xdist, Appium (XCUITest, UiAutomator2), Selenium, Xcode simulators, Android emulators and real devices

Open the full screenshot: Two terminals beside two iPhone simulators running a grocery app.
Two iOS simulators running the suite in parallel, with the test runner above and the Appium server log below.
Open the full screenshot: A terminal listing failed tests beside a Chrome window under automated control.
A Selenium run in Chrome. The terminal lists the failed tests by name, ready for a rerun of only those tests.

Field note

A hybrid app is two apps in one: a native shell with a website inside it. The test tool treats them as separate worlds and can look in only one at a time, so a correct locator finds nothing when the test is in the wrong one. On one white-label iOS app the web side did not appear at all until I changed one setting in the test setup. The same framework had worked on the base app the day before.

Framework design and test reliability

Result. The suite went from 489 fixed pauses to 177, and one iOS test went from 156.7 seconds to about 77.

The framework follows the Page Object Model (POM), in four layers. A test reads like a user story. A container creates the component classes and hands them to the test. The components are the page objects, and each one keeps its own selectors. Every click, wait and context switch goes through one navigation core. Each retailer is a small project that inherits from the base project and overrides only what differs, with no copied files.

Framework layers Tests call a container, which wires component classes, which call one navigation core, which drives Appium or Selenium. Tests also call an API client for setup and checks. Retailer projects inherit from the base project. Tests (one user story each) Container: wires everything Components + their selectors Navigation core: click, wait, context Appium / Selenium API client Backend Base project Retailer project inherits
What the user does goes down the left side. Test setup and the checks on prices and totals go through the API on the right.

One bug in detail: the click that reported success and did nothing

Symptom. Buttons inside the app's web components highlighted when clicked, and nothing happened. A JavaScript click as a fallback seemed to fix it. Then other tests started to fail in a new way: a quantity went up by two, and a checkbox ended up where it started.

Cause. The app is built from custom elements with nested shadow DOM. In a webview the driver does not send a real tap. It runs a script that creates the click event, and that event is not composed, which means it stops at the shadow boundary and never reaches the handler on the outer element. The JavaScript click is composed, so it does arrive. The check that decided whether the first click had worked could not see inside nested shadow roots, so it reported a miss when the click had landed, and the fallback clicked again.

Fix. I changed the core, not the tests. A click is now one click on the element itself, and a listener on the document confirms that it escaped the shadow root. For controls that change again on a second click (checkboxes, steppers, buttons that open a dialog), the helper reads the state, clicks once, and waits for the state the test asked for. For anything that is saved, the test also checks the API, because the screen can show a checked box while the server says false.

Waits, retries and long tests

  • I replaced fixed pauses with waits for a real state. The 177 that remain sit in loops that check again anyway, where a short pause costs less than another round trip to the device.
  • A failing test is rerun once immediately, then once more in isolation, then it is reported: three runs at most. A test that fails because of a known app bug stays red, with the bug written in the test.
  • I split a 27-minute checkout test into three independent tests of 12 to 15 minutes. The total is longer. The gain is that a failure now costs a rerun of one part, and each part reports its own cause.

Tools. Python, pytest, pytest-check, pytest-rerunfailures, Allure, Appium, Selenium

Field note

The biggest single speed gain came from one setting. With a 10-second implicit wait, every check for an element that was already gone blocked for the full 10 seconds. The sign was in the log: every miss took between 10.0 and 10.6 seconds. I set the implicit wait to 0 and used explicit waits everywhere.

AI-assisted testing with coding agents

Result. A new retailer's test suite became stable in 1 week, where it used to take about a month. Stable means that every test without a known failure passed in the regression build.

My employer asked its engineers to use AI to work more efficiently. I work with an AI coding agent, Claude Code, every day, across the whole test cycle and not only to write code:

  • Test automation. Writing page objects and tests to the framework's standards, and finding the cause of flaky tests from the logs.
  • API and data. Through Playwright MCP the agent opens a page for me, so I can trace which API calls a screen makes, see how the page is built, and check the data behind it.
  • Performance. Reading load test and profiling reports, and working out what each metric means for the user.
  • SEO and accessibility. I wrote my SEO crawler script and my accessibility report generator with AI assistance.
  • Reports. Drafting test reports and handover documents, which I then check and correct.

The agent is fast, so the control around it matters more than the prompt. I wrote rule files and hooks that hold its output to the framework's standards, I keep a written playbook of working rules, and I read the diff of every change before it is pushed.

I also trialled a shared local model: LM Studio on a Mac mini, holding one knowledge base of product testing that teammates could reach from their laptops without hosting a large model themselves. It did not go into regular use.

Tools. Claude Code, custom rules and hooks, Model Context Protocol (MCP) servers such as Playwright MCP, LM Studio

Field note

An AI agent solves the problem in front of it, not the one you have next month. More than once mine got past a hard step with a block of raw JavaScript. It worked that day, and nobody could have maintained it. I pointed it back to the framework's own functions. The rule files exist for cases like this.

API testing

Finding. A 44-case retest of sign-up email validation showed that the UAT environment was stricter than staging and rejected real addresses, such as names with a hyphen. I reported it for a product decision, with the table of cases.

I test REST APIs by hand in Postman first, then turn the checks into automated ones. I built the framework's API client for this. Tests create their own accounts, carts and addresses through the API, and drive only the behaviour under test through the screen. Money shown in the app is compared with the API's summary, not scraped number by number.

Tools. Postman, Python, pytest, REST

Open the full screenshot: A Postman request to a payment endpoint with a 404 response and the request shown as a curl command.
The same payment endpoint called with tokens of different privilege levels, to confirm which callers the server refuses.

Field note

In an early load test run, every login returned HTTP 200 and the failure rate showed 0%. The body said the username or password was incorrect. Only a check on the body's success flag caught it, so now every request asserts both the status and the flag.

CI/CD and reporting

Result. One regression build ran 211 test functions in 84 minutes with no failures. A test function can cover several related test cases on the same flow, which saves run time.

I built the regression plan on Atlassian Bamboo, the team's continuous integration (CI/CD) server, and my suites ran in it in stages: cleanup, setup, parallel test jobs per feature area, a rerun of failures, an upload to the test management tool, and an Allure report. A failure in the report links to its ticket, so the next person can see whether it is new or already known. Bamboo is the only CI server I have used. Its building blocks (stages, parallel jobs, a rerun step, a published report) also exist in Jenkins, GitHub Actions and GitLab CI.

Tools. Bamboo, Bitbucket, Git, Allure, TestRail, Jira

Open the full screenshot: A Bamboo build summary with a green success banner and test counts.
A regression build: 211 tests, 84 minutes, no failures.
Open the full screenshot: A Bamboo build summary with a red failed banner and counts of new, existing and fixed failures.
A build that failed. None of the failures were new: 11 were already known and linked to tickets, and 17 tests had been fixed since the previous run.
Open the full screenshot: An Allure report with a tree of test suites and the steps of one passed test.
An Allure report. Left: suites per feature area with pass and fail counts. Right: one test broken into timed steps.

Field note

A rerun is not a fix. My runner once had a second, parallel retry between the first rerun and the final one. It mostly ran the same failures again and made the build slower, so I removed it. A test now runs three times at most.

Performance and load testing

What it gives the team. "The app feels slow" becomes a number with a pass or fail line on it, and a slow frame becomes a named step in a named test.

With my team I worked on k6 load tests that replay a 21-step shopper journey against the platform's API, from sign-in to checkout and order history, across 4 retailers and 3 environments. I ran the production tests. They were authorised by the platform manager, and their figures stay with the company.

For Android I built a runner that wraps the test suite and profiles the app one test at a time with Flashlight: frame rate, CPU and memory, matched to the test step that was running.

Tools. k6, JavaScript, Flashlight, Python, pytest

Open the full screenshot: A line chart of frame rate and CPU use over 79 seconds, with dashed lines at each test step.
Frame rate (green) and CPU (yellow) across one test, with a dashed line at each step. The full image lists the step that was running at each of the slowest frames.
Open the full screenshot: A performance score of 99 with average frame rate, CPU and memory figures.
The result for one login and logout test on Android: score 99, 58.8 frames per second on average.

Field note

People notice delay at three fixed points: about 0.1 seconds feels instant, about 1 second keeps their train of thought, and about 10 seconds loses their attention. In one of my test runs the slowest 5% of calls took just over 1 second, so about 1 call in 20 crossed the 1-second limit. An average would have hidden that.

Security testing

Many real security bugs are not clever hacks. They happen when the server trusts what the app tells it. So I check that the server, not the app, enforces the rules. I put a proxy between the app and the server, edit a request or a response on its way, and see whether the system still refuses what it should refuse. I did this for payments and for one-time passcodes, on my employer's own platform, as part of release testing and its PCI-DSS v4 certification work.

Editing traffic is one check among many. Some of the checks I run:

  • Broken access control. Can one user reach another user's data?
  • Session handling. Does a token stop working after logout?
  • Rate limiting. Can an endpoint be called again and again with no limit?
  • File upload. Are dangerous file types refused?
  • Race conditions. Do requests sent at the same moment break a rule, such as uniqueness or single use?
  • Injection. SQL injection and cross-site scripting (XSS).

When a defect is fixed, I test it again to confirm that the fix works as expected.

To inspect the Android app's traffic I rooted an emulator and installed the proxy's certificate as a system certificate, then wrote the steps down so the rest of the team could do the same.

Tools. Burp Suite, OWASP ZAP, Postman, Android emulator, Magisk

Open the full screenshot: A proxy tool showing an edited JSON request on the left and the server's JSON response on the right.
A payment request held in the proxy and edited before it reaches the server (left), with the server's answer (right).
Open the full screenshot: A proxy tool showing a request on the left and an edited response on the right.
A response edited in the proxy before the app receives it, to check whether the app trusts what it is told.

Field note

Since Android 7, an app does not trust a certificate that the user installs unless the app itself opts in. The first time I pointed the app at a proxy, it loaded nothing at all. For a build I could not change, the proxy's certificate had to go into the system store, which on an emulator means rooting it first.

SEO and data checks

Finding. On one retailer's site, product pages looked complete in a browser and were blank to a crawler. The site waited for a location permission to choose a store, and a crawler never grants one. The fix was a default store. I confirmed it with my script and with Google's Rich Results Test, and wrote the audit report.

For search engine optimisation (SEO) checks I wrote a Python script that fetches the raw HTML of a page, with no cookies, no JavaScript and no permissions, and grades only what that first response holds: the head tags, the structured data and the text in the HTML.

For data I query SQL Server and MySQL to prepare test data and to check what is behind the screen.

Tools. Python, requests, BeautifulSoup, Lighthouse, DBeaver, SQL Server, MySQL

Open the full screenshot: A terminal report of structured data checks beside a browser SEO extension showing scores.
One product page checked three ways: my crawler script in the terminal, a browser SEO extension, and Lighthouse in the full image.
Open the full screenshot: A DBeaver window. Everything below the title bar is blurred.
Database checks in DBeaver against SQL Server. Content hidden.

Field note

A page can pass every check you run in your own browser and still be invisible to search. Your browser has cookies, a location and JavaScript. Test the page the way the crawler meets it: cold, with none of them.

Accessibility testing

Result. When accessibility problems were reported on one retailer's site, I reproduced each one, proposed the fix and verified that the fix met the reporter's expectations.

I built an audit test that scans each page with axe against WCAG 2.1 A and AA, in phone, tablet and desktop sizes, and runs Lighthouse as well. It covers the pages a guest can open and the pages behind sign-in, and a page with more than 10 violations fails the run. Then I check by hand with VoiceOver, because a scan finds only some of the problems. In 2017 the UK government tested 10 scanning tools on a page built full of known barriers, the best tool found about 40% of them, and 29% were found by no tool at all. A scan also cannot tell you what a blind user hears first, or whether the order makes sense.

Tools. axe, Lighthouse, VoiceOver, Selenium, Python, pytest, Allure

Open the full screenshot: A grocery home page with numbered outlines on its first 14 screen reader stops, and a list of what is announced at each stop.
The order and the wording a screen reader gets on a retailer's public home page, read from the browser's accessibility tree. Stop 1 is the button on the notice banner.
Open the full screenshot: An accessibility report table with one row per page: scan time, page title, URL, violations by impact and a small screenshot.
The report my audit test generates: one row per page, with the violations by impact and a screenshot. Shown here on a retailer's public site at phone size; the full image lists each violation.

Field note

A cookie banner or an important notice should come first in a screen reader's reading order, wherever it sits on the screen. A banner pinned to the bottom of the page often comes last in the reading order, so a blind user goes through the whole page before hearing about it. For any business that serves the public online, that is a legal risk, not only a usability one. It is why I check the reading order and not only the scan result.

Working with a team

My direction is Quality Assurance (QA) Lead. I have not held that title yet. These are the parts of a lead's work I already do:

  • Mentoring. I onboarded and mentored 4 new team members on the system and on testing practice. Each was working independently within about a month.
  • Guides. I wrote 8 step-by-step guides for the team, from setting up the frameworks on Windows and macOS to inspecting app traffic.
  • Test design. I write test plans, test matrices and UAT reports, run grey-box tests around developer fixes, and track each bug from report to closure.
  • Manual testing of payments. Card entry, credit card and EBT tenders, gift certificates, CVV and AVS responses and point-of-sale transactions, plus coupons, promo codes and loyalty points checked against the API.
  • Build before buy. I tried BrowserStack for parallel web and mobile testing and for accessibility testing during a trial. It is a paid product, so my QA manager asked whether I could build the same coverage in-house. I did, with free tools: the framework runs app tests in parallel on several simulators and web tests in several browser sessions at once, and my audit test does the accessibility scan.
  • Working across countries. From Vietnam I worked directly in English with colleagues in the US and Canada, including my QA manager: in meetings, in chat, and in person when they visited.
  • Ownership. I owned the retailers' test suites on web and mobile from the first test to the regression build, and the guides above let someone else run and extend them.

Building a product with other people

I also build products with other people. One person rarely has every skill a product needs, so each of us covers what the other lacks. When someone brings me an idea, I work on it with them: I ask questions until the requirement is clear, suggest improvements, build it with an AI coding agent, and test it with the same care I give to a retailer's app. The idea stays theirs.

Write it: Chinese is one of these. Someone shared the idea with me and asked me to help make it real. It is an app for learning to write Simplified Chinese by hand, for English and Vietnamese speakers. You draw a character with a finger or a mouse and the app recognises it. A challenge asks you to write a whole sentence, then marks each character. It is a proof of concept and is not published yet.

  • Scope. We set it together: 713 characters from HSK levels 1 to 3 plus common basic characters, 20 sentence challenges, an English and a Vietnamese interface, and no server, so it works offline.
  • Build. Claude Code writes the code from a written brief and an architecture document. One TypeScript codebase runs as a website and as an Android app.
  • Testing. I write characters the way a learner does: wrong stroke order, joined strokes, look-alike pairs such as 人 and 八. Each attempt that fails is saved with its exact strokes and becomes a permanent regression test.
  • Automation. Playwright tests draw characters stroke by stroke and check the layout at 6 screen sizes, from a 320 px phone to a desktop.

Tools. Claude Code, TypeScript, Preact, Vite, Capacitor, Vitest, Playwright

A 25-second recording with no sound. The challenge asks for "I'm Vietnamese." in Chinese. Each handwritten character is recognised after a short pause, and the answer 我是越南人 is marked correct. The strokes are drawn by a Playwright script that adds a hand's wobble.

Field note

The first automated tests passed, and then the app recognised nothing I drew with a real mouse. The tests had drawn clean strokes with a few points. A real hand makes about 300 points with a wobble in them. After the fix, recognition went from 0% to 98% or more across the character set, and a regression test now draws dense, wobbly strokes.

Contact

I am in Ho Chi Minh City and available now. I am open to roles in Ho Chi Minh City, to remote roles, and to roles abroad with relocation, which I would prefer. Tell me about the role and I will reply as soon as I can with a CV written for it.

LinkedIn profile

The screenshots on this page show my own work, with hosts, accounts and names hidden.