There is a fairly common route from Manual Tester to Automation Tester:

  1. Install Selenium or Playwright.
  2. Learn how to find an element.
  3. Successfully click the Login button.
  4. Post a screenshot of the green report online.
  5. Three weeks later, the Login button gets a new id, and the whole test suite turns red like a Christmas tree.

At this point, many people conclude that automation is hard, this tool is rubbish, and that other tool is the one true answer. Delete the project, install a new tool and start clicking Login from the beginning. A rather RAM-expensive reincarnation cycle.

The problem is usually not the button. It is that we bring the entire manual way of working into code: do whatever we see, write whichever case is in front of us, celebrate when it runs, and worry about who will maintain it six months from now... six months from now.

That is why the first article in the From Manual to Fullstack series will not rush into teaching you a single line of code. I want to talk about what needs to change before you choose a language, tool or design pattern: your mindset.

What is test automation?

In short, test automation means using software to perform actions, collect results and compare the actual result with the expected result according to a defined scenario.

The part worth noticing is defined. A tool does not know by itself what a successful login means. Did the URL change to /dashboard? Did the username appear? Did the API return 200? Was a token saved, or was a new session created in the database? The person writing the test has to choose which signal is enough to reach a conclusion. If the test oracle is wrong, the machine can run as quickly as it likes; it only helps us be wrong faster.

Concept note: A test oracle is the basis used to decide whether a result is right or wrong. It may come from a requirement, a business rule, a known calculation or a confirmed system state.

From the system's side: the same set of effects

Suppose you test login by hand:

Concept note: In plain terms, HTTP is how a browser and a server talk to each other. The browser sends a request asking for something to happen, and the server returns a response saying what happened. The specific address receiving the request, such as POST /api/login, is commonly called an endpoint. Just understand that behind a single click there is usually an exchange the user's eyes cannot see.

UI automation takes the same route. Selenium or Playwright controls the browser to enter data and click the button, then the browser still sends an HTTP request. The server on the other side does not sit there trying to guess whether the button was pressed by a finger or poked by WebDriver. The effect the system receives is, in essence, still a set of requests, events and data changes.

API automation takes a shortcut: instead of opening the UI and waiting for the browser to create a request, the test calls the endpoint directly. It cannot check whether the button has slid over to the neighbour's house, but it checks business rules and responses faster, with less dependence on the interface.

So from the system's point of view, manual and automation are not two opposing kinds of magic. Both affect the system through the interfaces it provides. The difference is who or what creates the effect, which layer we observe, and how we collect the evidence.

From the practitioner's side: very different indeed

Manual testing relies heavily on observation and the ability to adapt at the time of testing. If a popup appears unexpectedly, the tester reads it, closes it and moves on. If the label changes from “Login” to “Continue”, the eye catches it immediately. If the total looks a little suspicious, the tester can stop, open DevTools and dig further.

Automation is not that flexible unless we teach it to be. A script needs locators, data, wait conditions, assertions and a sufficiently clear way to handle state. In return, once it is written properly, the script can repeat the same flow dozens or hundreds of times without, on run number 37, suddenly forgetting to check the toast because it is thinking about what to eat for lunch.

Concept note: A locator is how a script identifies the exact element it needs to interact with. An assertion checks whether the actual result matches what we expect. State is the system's current condition, such as the user being logged in, the account being locked or the order having been paid.

Moving to automation is therefore not a matter of replacing hand-clicks with code-clicks. We have to turn the thinking in a tester's head into something a machine can repeat, while turning the test project into a code product that needs to be designed, reviewed and maintained.

UI, API and hybrid models

We can loosely divide automation by the interface the test acts on:

Concept note: In an API response, the status says whether the request succeeded or failed, the headers contain accompanying information, the body is the main data returned by the server, and the schema describes the structure that data must have.

MDF here should be understood as the team's particular implementation, not a universal standard that appears when you install a package. What matters is the problem that abstraction layer solves. MDF might let a team share a flow between Android and iOS, hide some Appium details, manage devices, or combine UI with APIs. If a grand-sounding acronym cannot answer that question, it is still a bit of Canadian magic.

Concept note: In plain terms, abstraction means putting an easier-to-use layer on top so that test writers do not have to touch every fiddly detail underneath.

Test automation tools

The word framework gets used quite freely in automation. JUnit is a framework, Selenium also gets called a framework, and the codebase a team builds itself is also named a framework. That is not necessarily wrong in every context, but if we do not separate the roles, choosing a solution becomes a case of one person saying chicken while another installs yet another dependency.

A test framework organises and runs tests

JUnit, TestNG and pytest belong to the test-framework group. They give us things such as:

Concept note: Setup prepares things before a test runs; teardown cleans up afterwards. A fixture packages and provides things a test needs, such as a browser, user or data. A hook is an insertion point for running some processing before or after a test, suite or particular event. Names differ between frameworks, but basically these features save us from copying the scene of opening the door, putting out the furniture and sweeping the floor into every test case.

They understand what @Test, assert and a fixture mean, but they do not inherently know how to click a button in Chrome or swipe an Android screen. Handing JUnit an XPath locator and staring at it will not make the button click itself.

An automation tool or library touches the system

Selenium and Playwright control browsers. Appium automates UI on mobile and various other application platforms. REST Assured provides a Java API for sending requests to and checking REST APIs. Karate has a DSL for API testing, assertions, mocks and additional capabilities in the same ecosystem.

A few common combinations:

NeedTest frameworkInteraction tool/library
Web UI with JavaJUnit or TestNGSelenium
API with JavaJUnit or TestNGREST Assured
Web/mobile with PythonpytestSelenium or Appium client
Mobile with JavaJUnit or TestNGAppium client

Then there are choices that look closer to all-in-one. With TypeScript or JavaScript, Playwright Test includes a runner, fixtures, assertions, reports, browser automation and APIRequestContext for calling APIs. Karate provides a DSL, assertions, HTTP calls, mocks and execution through JUnit or the CLI; an API project may not need to manually join as many pieces as a REST Assured + JUnit combination.

“All-in-one” does not mean that one tool can handle every front. Playwright is very strong for web and API work, but that does not turn it into the default choice for native mobile. Karate has UI automation, but a team being good at Karate API does not automatically prove it is the best answer for every complex web UI. Appium can touch mobile, but it still needs a test runner, project architecture, reporting and device-management practices around it.

Separating these roles helps us ask the right questions:

If we do not ask, it is very easy to choose a tool because an eight-minute login tutorial works nicely, then reach the point where we need to test OTP on two operating systems and discover that the best part of the tutorial was the background music.

Is automation worth the money?

Automation is not free. It simply moves the cost from “a person sitting there and repeating the work” to “a person building, running and maintaining something that will repeat the work”.

A manual test might take ten minutes. Automating it might take several hours or days, plus time to build the environment, create data, connect CI, read reports and fix things when the product changes. If the flow will run exactly once and then be abandoned, automation may be like buying a dishwasher to wash one spoon.

The return comes from repetition. A regression suite that runs every release, a smoke test that runs in several environments, or one rule that needs checking against many data sets or browsers: the more often it runs, the more chance the initial investment has to be spread out. The machine can even run at night, as long as the test environment does not fall asleep first.

That is when we need to look at ROI. Automation has a good ROI not because the report looks polished, but because the value returned across repeated runs exceeds the cost of building and keeping it alive.

Concept note: ROI stands for Return on Investment. In plain terms, it is how much effort and money we put in, and how much value we get back.

To talk about ROI properly, we need to count at least:

Value saved across runs - build cost - maintenance cost - operating cost

Do not compare one automation run with one manual run and announce a 90% saving. Two minutes of green does not tell you about three days spent building the framework and two hours on Monday morning fixing test data that someone else has used up.

“Can be automated” is different from “should be automated”

Technically, quite a lot can be automated if the time and budget are wide enough. The more useful question is whether it should be.

A candidate is often worth automating when it:

On the other hand, a flow that changes every week, runs once, needs aesthetic judgement or requires a person to explore the unknown may be more economical to test manually. Not automating a case is not a moral failure. Sometimes it is simply knowing where to put the money.

CAPTCHA is a beautiful example. It was created to tell humans and bots apart. Using a bot to solve CAPTCHA in every regression run means we are spending money building a machine to fight the exact mechanism designed to stop machines.

Instead of researching how to make Selenium recognise a crosswalk, QA can talk to developers about creating a mechanism specifically for the test environment. For example, developers can mock the CAPTCHA service so that it returns pass or fail according to data we choose. An even simpler option is to turn off the real CAPTCHA in the test environment and replace it with a CAPTCHA passed checkbox. Automation only needs to tick that box to continue; to test the failed-CAPTCHA branch, leave it unticked or select a fail value.

This way, the regression suite still checks how the system handles CAPTCHA passing or failing, without calling the real service or teaching a bot to pretend to be human. The integration with the real CAPTCHA can remain in a separate test group if the risk requires it, instead of making the entire suite solve a puzzle on every run.

Of course, this checkbox or bypass must only be enabled by configuration for the test environment and must not exist in production. If production also has a CAPTCHA passed checkbox, automation will run very smoothly, and I imagine the hacker will give the user experience five stars too.

Concept note: A mock or stub replaces a real dependency with a test version that returns results we control. A bypass lets a test flow go around a checkpoint in a way limited to the test environment. Neither means deleting security for convenience and hoping the config file does not reach production. Both must be designed, access-controlled and separated from the real environment.

OTP works in much the same way. Instead of waiting for an SMS on every regression run and reading the code from a phone plugged in at the corner of the room, the team can stub the provider, use a test endpoint or use an OTP retrieval mechanism reserved for the test environment. The goal of most tests is to check the system's authentication flow, not to check whether the mobile network happens to be in the mood to send a message today.

For OCR or face recognition, the ordinary business flow can mock recognition results so that pass, fail and retry branches are checked consistently. The OCR or face-recognition model itself still needs testing with a controlled data set, its own thresholds and metrics. Mocking a dependency does not mean skipping the dependency's tests; it simply stops a checkout test from also having to work as an eyesight examiner.

How do we carry out test automation?

Once we have chosen what is worth automating, we can finally get to the code.

Have a runner, have a tool, then build the project properly

A basic project needs a test framework to organise test execution and an automation tool/library to interact with the system. The team then builds a structure around them so that a test case does not take on every responsibility.

For example, a project could be split like this:

tests/       # Business-level scenarios and assertions
pages/       # Interaction with each UI page or component
services/    # API clients and service-level business operations
fixtures/    # Initialising state, users, browsers and devices
data/        # Controlled test data
config/      # Environment-specific configuration

Not every project needs exactly these six folders. A project with five tests that builds seventeen layers is not scalable; it is a three-storey house built for one cat. The point is that where locators live, where APIs are called, where test data is kept and where assertions are made should be clear enough for the next person to find.

Reuse requires thought too. Do not see three identical lines and immediately create CommonUtilsHelperManagerFactory. Group behaviour that has the same meaning and the same reason to change. loginAs(user) may be worth reusing because many tests need to log in. A function that takes twelve booleans to log in, create an order, take a screenshot and sometimes log out has been reused to the point where nobody dares use it anymore.

Page Object Model for UI, service layers for APIs

Concept note: OOP stands for Object-Oriented Programming. In plain terms, it is a way of organising code around objects; each object keeps the data it manages and the related actions together. Imagine that instead of leaving the keys, vehicle registration and driving instructions in three different rooms, we keep them around a Car object. OOP also has classes, inheritance, encapsulation and plenty more, but this article does not need to carry the whole textbook in here.

For UI, the Page Object Model applies the organisation of OOP by grouping the locators and actions of a page or component into an object. The test calls loginPage.loginAs(user) instead of finding the email input, password input and submit button in every file. When the UI locator changes, there is one sensible place to fix it.

POM should not swallow the entire business flow. If a CheckoutPage selects products, creates a user through the database, calls an API to get a voucher, pays and then verifies an email, the Page Object has evolved from “representing one page” into head of an all-purpose department.

For APIs, the equivalent approach is to organise clients by service or resource: AuthService, OrderService, PaymentService. Each service knows its endpoint, auth and request/response mapping; the test keeps the business scenario and meaningful assertions. POM and service-layer design have plenty of places where it is easy to overdo things, so I will dig into them separately in another article.

Design from the need, not from the tool's name

Suppose the product only has an API. Setting up Selenium Grid because “a proper framework must have UI” is drawing legs on a snake. If the product is a native mobile app on Android and iOS, REST Assured can run as fast as it likes; it still cannot check gestures, permissions or a keyboard covering the button.

There are trade-offs within mobile too. Using Appium directly gives the team full access to the driver API, makes documentation easier to look up and keeps debugging close to the source. In return, the tests may become tied to many capabilities, locators and platform differences.

Going through a team MDF layer may let test cases use business-level keywords, reuse flows and hide some Android/iOS details underneath. In return, every abstraction is another layer to learn, debug and maintain. When Appium adds a new feature that MDF has not exposed, the team either waits or punches through the abstraction—and from there the project starts having two routes.

There is no “best framework” answer outside its context. There is an answer that fits the app, the team's skills, CI, device farm, available time and acceptable maintenance level better. Choosing a solution means choosing both the benefits and the lump of debt that comes with it. If you only watch the demo and never inspect the lump of debt, the instalment payment can be a surprise.

Concept note: CI stands for Continuous Integration, a system that automatically builds and runs checks when code is put into a shared repository. A device farm is a managed group of real or virtual devices for running mobile tests across multiple configurations.

Working rules

An automation project is a codebase where several people live together. A test that runs correctly on my machine is the entrance requirement, not a certificate of good citizenship.

Code needs to be readable, naming needs to be consistent, assertions need to say clearly what is being checked, and careless copy-paste should be avoided. But optimising reuse also has to come with the ability to understand and fix things. Do not compress five business steps into a mysterious helper just so the test case can be three lines long and look sophisticated.

Responsibilities in the project should be separated clearly enough. Someone fixing a locator should mainly touch the page or component. Someone adding an endpoint should work in the corresponding service. Test data and configuration should not be scattered a little at a time through every file. When two people working on two different features both have to edit a 2,000-line BaseTest.java, the conflict is no longer an incident; it is a regular appointment.

Concept note: A Git conflict happens when Git cannot decide how to combine changes, usually because two people edited the same area of code in different ways. A person then has to choose which part stays, which part goes or how to rewrite it so both changes can live together. A conflict is not always somebody's fault, but a huge file that everybody has to touch invites conflicts over for tea more often.

Git conventions also need agreement: understandable branch names, small commits, one commit focused on one meaningful change, and a message that says what was done. A commit called update code final final 2 may run, but three months later even git blame can only tell that its author was once very desperate.

Concept note: A branch is a separate working line for developing a change. A commit is a recorded point containing a group of changes and a description. git blame is a command for tracing which commit changed a line of code and who made that change.

Small commits make review easier, reverting less painful and tracing the change that broke the tests faster. Do not combine locator changes, project-wide formatting, a dependency upgrade and 40 new tests into one lump, then invite a colleague to “review it, it is just a little thing”.

The rule I want to keep for this series is quite simple:

Collaborate, support each other, and do not get in each other's way.

If an abstraction lets you write faster but leaves three other people unable to debug, it is not an optimisation for the team. If a convention looks beautiful on paper but makes everyone take the long way round every day, that convention needs changing. Good automation must work with the system; it must also survive alongside the people maintaining it.

Conclusion

When moving from manual to automation, the biggest change is not replacing a mouse with Selenium or Playwright. We start seeing tests as code, frameworks as a long-term operating solution, and each automation candidate through the question of ROI.

Tools still matter. Choosing the wrong tool really is tiring. But chasing tools before knowing which layer needs testing, how often it will be repeated, who will maintain it and which trade-offs are acceptable is like buying a complete set of tools and only then wondering what in the house needs repairing.

Automation starts creating value when our thinking moves from “how do I make the machine run this case?” to “how do I make the whole test system run correctly, run again and not become the team's lump of debt?”

Code comes afterwards. Changing the way we think is the opening scene.

References