Why Project Structure Matters More Than You Think
When you’re excited to start automating tests with Robot Framework, it’s tempting to just create a few test files and start writing. But here’s what I’ve learned after setting up dozens of test automation projects. The decisions you make in the first hour of setup will either make your life easier for months to come, or they’ll create friction every single day. A well-structured project isn’t about following rules for the sake of it. It’s about making sure that six months from now, when your test suite has grown from ten tests to a thousand, you can still find what you need, add new tests without breaking old ones, and onboard new team members without spending days explaining where things are.
This guide will walk you through setting up a Robot Framework project the way professional QA teams do it. We’ll focus on creating a foundation that scales, using modern tooling that makes environment management painless, and organizing code in a way that actually makes sense when you’re maintaining it.
Understanding What We’re Building
Before we start creating folders and installing packages, let’s talk about what a well-structured Robot Framework project actually looks like. Think of your test project as having several distinct layers, each with its own responsibility. You have your test cases themselves, which describe what you’re testing. You have reusable keywords that encapsulate common actions. You have locators that define how to find elements on pages. You have test data that drives your tests. And you have custom libraries that extend Robot Framework’s capabilities.
The key insight is that these layers should be separate. When you need to update a locator because the development team changed an element’s ID, you shouldn’t have to hunt through test files. When you want to reuse a keyword across multiple test suites, you shouldn’t have to copy and paste code. A good project structure makes these kinds of maintenance tasks trivial instead of painful.
We’re also going to use uv, a modern Python package manager from Astral, to handle our dependencies. If you’ve worked with traditional Python virtual environments and pip, you know that reproducing the exact same environment on a colleague’s machine or in CI/CD can be surprisingly tricky. Version conflicts, platform-specific dependencies, and the infamous “works on my machine” problem are all too common. uv solves these problems by creating truly reproducible environments. When you set up a project with uv once, anyone can recreate that exact environment anywhere, every single time.
Getting Started with uv
The first step is installing uv itself. Head over to the uv documentation and follow the installation instructions for your operating system. On most systems, it’s a simple curl command or installer download. Once you have uv installed, you can verify it’s working by running uv --version in your terminal.
Now let’s create a new project. Navigate to where you want your project to live and run uv init robot-test-project. This creates a new directory with a basic Python project structure. Change into that directory with cd robot-test-project. You’ll notice uv has created a pyproject.toml file for you. This file is going to be the heart of your project configuration. It defines your dependencies, project metadata, and can even contain scripts for running your tests.
Open up the pyproject.toml file and let’s add our core dependencies. You’ll want to add a dependencies section that includes robotframework as the foundation, pyyaml for managing locators in YAML files, and openpyxl for working with Excel test data files. If you’re planning to use a data-driven approach where one test case runs with multiple sets of data, add robotframework-datadriver as well. Your dependencies section should look something like this:
[project]name = "robot-test-project"version = "0.1.0"dependencies = [ "robotframework>=7.0", "pyyaml>=6.0", "openpyxl>=3.1.0", "robotframework-datadriver>=1.9.0",]Once you’ve defined your dependencies, run uv sync to install everything. This command creates a virtual environment, installs all your dependencies, and generates a lock file that pins exact versions. This lock file is crucial. It means that when your teammate or your CI/CD pipeline runs uv sync, they’ll get the exact same versions of every package that you’re using right now. No more subtle bugs caused by version mismatches.
Choosing Your Project Structure: BDD or Traditional
Robot Framework projects typically follow one of two organizational patterns, and choosing between them depends on how you want to write your tests. The traditional approach organizes tests into test suites with keywords and resources. The Behavior-Driven Development approach uses feature files written in Gherkin syntax with step definitions. Both are valid, and your choice should be based on whether you’re collaborating closely with non-technical stakeholders who need to read and validate test scenarios.
Let’s start with the BDD approach since it requires a bit more explanation. In BDD, you write your tests as feature files that read like plain English. A feature file describes a user scenario in terms of Given, When, and Then steps. For example, a login feature might say “Given I am on the login page, When I enter valid credentials, Then I should see the dashboard.” These feature files don’t contain any implementation details. That’s the beauty of BDD. Anyone can read a feature file and understand what’s being tested.
The implementation lives in step files. Each step in your feature file corresponds to a keyword in your step file. When Robot Framework runs a BDD test, it reads the feature file, finds the matching step definitions, and executes them. Here’s the important part that trips people up. Before you start writing step definitions, you should commit to your feature file structure. Look across your features and identify steps that appear in multiple scenarios. These become your reusable keywords. If “I am on the login page” appears in five different feature files, you should have one keyword that implements it, not five separate implementations.
Create a features directory in your project root. This is where all your feature files will live. Create a steps directory as well, which will contain your step definition files. Each feature file imports only its corresponding step file, and the step file is where all the actual Robot Framework keyword implementations live.
For the traditional approach, you skip the features and steps directories entirely. Instead, you create a tests directory that contains your test suite files. These files use Robot Framework’s standard syntax with test cases and keywords directly in the same file or imported from resource files.
Building the Foundation: Resources and Custom Libraries
Regardless of which approach you choose, you’re going to need a resources directory. This is where you keep things that multiple tests need to access. Think of it as your shared utilities folder. Inside resources, create separate subdirectories for keywords, locators, and test data. This separation is important because different team members might work on different parts. Your automation engineers might update keywords while your QA analysts manage test data, and you don’t want them stepping on each other’s toes.
Let’s talk about locators specifically because this is where I see teams make a critical mistake. You might be tempted to create one big YAML file with all your locators for the entire application. Don’t do this. When that file grows to hundreds of lines and something breaks, you’ll spend hours trying to figure out which locator is causing the problem. Instead, create separate YAML files for each page or major component of your application. If you’re testing a web app with a login page, dashboard, and settings page, you should have login_locators.yaml, dashboard_locators.yaml, and settings_locators.yaml.
At the top of each locator file, add a comment that identifies which page it belongs to. This seems obvious when you’re creating the file, but trust me, six months later when someone is debugging a test failure at midnight, that comment will save them precious time. Inside each YAML file, structure your locators in a way that makes sense for how you reference them. A simple flat structure with descriptive keys often works best.
Now let’s talk about the lib directory. This is where you put custom Python libraries that extend Robot Framework’s functionality. You might create a library for specific API interactions, database queries, or custom assertions. The question is how do you make Robot Framework aware of these libraries? You have two options, and the right choice depends on your use case.
If you’re writing simple utility libraries that are specific to one project, the easiest approach is to use the --pythonpath argument when running your tests. This tells Robot Framework to look in that directory for imports. You’ll add this to your executor scripts, which we’ll get to in a moment.
If you’re maintaining a project-wide repository of libraries that multiple test projects use, or if your libraries are complex enough that they deserve proper versioning and packaging, then you should build them as proper Python packages. With uv, this is straightforward. You structure your lib directory as a Python package with an __init__.py file, add it to your pyproject.toml, and run uv pip install -e . to install it in editable mode. This approach is more work upfront but pays off when you need to share libraries across projects or manage versions carefully.
Managing Secrets and Environment Configuration
Here’s something that catches a lot of teams by surprise. You can’t just hardcode credentials and API keys in your test files. Obviously you know this, but the question is how do you actually manage secrets properly? The pattern we follow is simple but effective. All secrets and environment-specific configuration should be stored as environment variables, and your tests should read from those environment variables.
The naming convention for environment variables matters more than you might think. Use all uppercase letters, and start with the platform or service name. If you’re storing an Azure subscription key, name it AZURE_SUBSCRIPTION_KEY. If you need an API endpoint URL, call it API_BASE_URL. This convention immediately tells anyone looking at the variable what it’s for and where it’s used. When you’re debugging why a test is failing in staging but working locally, you’ll appreciate being able to quickly scan environment variables and understand what they’re for.
For local development, you can set these environment variables in your shell or use a .env file with a tool that loads them. Just make absolutely sure that your .env file is in .gitignore so you never accidentally commit secrets to your repository. For CI/CD pipelines, you’ll store these in your platform’s secret management system. If you’re using GitHub, that means GitHub Environments. If you’re using Azure DevOps, it’s variable groups. The beauty of this approach is that swapping between environments becomes trivial. You’re running the exact same test code, just with different environment variables, and your tests automatically adapt.
In our organization, we have an internal library that encrypts secrets in test reports, so even if a password appears in a log message, it’s automatically redacted. You might not have that luxury, but you should still be careful about logging sensitive data. Make sure your keywords that handle authentication don’t log the actual credentials.
Creating Executable Scripts for Local Testing
Test automation is only useful if people can actually run the tests. This sounds obvious, but I’ve seen projects where running tests requires remembering a complex command with five different arguments in the right order. Don’t make people do that. Instead, create simple executor scripts that abstract away the complexity.
For Windows users, create batch files. For Mac and Linux users, create shell scripts. The idea is the same. These scripts should handle setting up the environment, running the tests with the right arguments, and collecting the results. A typical bat file might activate the virtual environment, set a few environment variables, run robot with the appropriate tags and output directory, and then maybe even open the report file when tests complete.
With uv, your executor scripts become even simpler because you don’t have to manage virtual environment activation manually. You can use uv run to execute commands in the project’s environment automatically. Even better, you can define custom scripts directly in your pyproject.toml file under a [project.scripts] section. These scripts are wrappers around robot commands that make common test execution patterns easy to invoke.
For example, you might create a script called test-smoke that runs only your smoke tests, and test-regression that runs the full regression suite. Users can then run uv run test-smoke from anywhere in the project and it just works. This approach is especially powerful in CI/CD where you want to minimize the amount of pipeline-specific logic.
Setting Up Data-Driven Testing
Many test scenarios need to run with different sets of input data. Testing login with valid credentials, invalid passwords, expired accounts, and locked accounts is really the same test logic with different data. This is where data-driven testing with the robotframework-datadriver library becomes powerful.
The way datadriver works is elegant. You write one test case as a template, and datadriver reads test data from an external file and runs your test case once for each row of data. You can use CSV files, Excel files, or even JSON files as your data source. I generally recommend Excel for test data because it’s easy for non-technical team members to edit, supports multiple sheets for organizing different test scenarios, and handles complex data types reasonably well. JSON is great when your test data has nested structures or when you’re programmatically generating test data.
Store your test data files in the resources directory, possibly in a dedicated testdata subdirectory. Name them descriptively so it’s obvious what test suite they belong to. If you have login test data, call it login_testdata.xlsx, not data1.xlsx. Future you will be grateful for descriptive names.
When you configure datadriver in your test suite, you specify the data file location and which column contains the test case name. Datadriver handles the rest, creating individual test cases for each row and passing the data as arguments to your template test case. The test reports will show each data-driven test separately, making it easy to identify which data set caused a failure.
Self-Healing Tests with Healenium
One of the biggest maintenance headaches in test automation is dealing with changing locators. You write a perfectly good test, it works great, and then two weeks later the development team changes a button’s ID or restructures a form, and suddenly half your tests are failing. The failures have nothing to do with actual bugs. They’re just broken locators. You end up spending more time maintaining tests than writing new ones, which defeats the entire purpose of automation.
This is where Healenium comes in, and it’s genuinely one of those tools that changes how you think about test maintenance. Healenium is a self-healing framework that integrates with Selenium-based tests, which includes Robot Framework when you’re using SeleniumLibrary or Browser library. Here’s how the self-healing flow works:
When a test tries to find an element using a locator and that locator fails, Healenium doesn’t just give up. Instead, it analyzes the page structure and tries to find the element using alternative attributes. It looks at things like the element’s position relative to other elements, its text content, its CSS classes, and other characteristics that might still be unique even if the ID changed.
When Healenium successfully finds the element using an alternative locator, it records this healing action. You can then review what Healenium healed and decide whether to update your locator files with the new values. This creates a workflow where your tests don’t break immediately when locators change. Instead, they keep running, and you get a report of what needed healing.
In our setup, each project gets its own dedicated Healenium pods that our DevOps team provisions. This isolation is important because different projects might be testing different applications with different healing requirements. Having separate pods means you can configure healing sensitivity per project and keep healing data isolated.
What makes this approach particularly powerful is that it works alongside your existing locator management strategy. You’re still maintaining clean YAML files with well-organized locators per page. Healenium isn’t replacing good locator practices—it’s a safety net that keeps tests running when locators change unexpectedly. The healing reports also give you valuable insights into which parts of your application’s UI are changing frequently.
Integrating with CI/CD: The Health Check Pipeline
Automated tests are most valuable when they run automatically. Setting up a CI/CD pipeline for your Robot Framework tests prevents regressions from sneaking into your codebase and gives you confidence that changes haven’t broken existing functionality. The pattern we follow is a two-stage health check that runs on every commit to development and main branches before merging.
The first stage is a dry run. Robot Framework has a --dryrun option that validates your test syntax, checks that all imported libraries and resources exist, and verifies that keywords are defined before executing any actual tests. This catches syntax errors, missing imports, and typo’d keyword names in seconds rather than minutes.
The second stage is the smart part. Instead of running your entire test suite on every commit, which could take hours for large projects, you run only the tests that were actually changed in that commit. This requires a bit of custom scripting to parse the git diff, identify which test files were modified, and pass only those files to robot. But the time savings are massive. You get fast feedback on whether your changes broke anything while still maintaining good test coverage.
For nightly builds or scheduled runs, you can run the full regression suite. This catches issues that might not be obvious from just the changed tests, like interactions between different parts of the system or environment-specific problems.
The key to making CI/CD work smoothly is that environment variable pattern we talked about earlier. Your CI/CD platform provides environment variables, your tests read from environment variables, so there’s no special configuration needed. You’re running the exact same test code locally and in the pipeline.
Bringing It All Together
Let’s walk through what your final project structure should look like. Here’s the complete folder structure for both BDD and traditional approaches:
BDD Project Structure
robot-test-project/├── pyproject.toml # Dependencies and project config├── uv.lock # Locked dependency versions├── .gitignore # Exclude venv, reports, secrets├── README.md # Setup and run instructions├── features/ # BDD feature files│ ├── login.feature│ ├── dashboard.feature│ └── settings.feature├── steps/ # Step definitions│ ├── login_steps.robot│ ├── dashboard_steps.robot│ └── settings_steps.robot├── resources/ # Shared resources│ ├── keywords/ # Reusable keywords│ │ ├── ui_keywords.robot│ │ └── api_keywords.robot│ ├── locators/ # Page-specific locators│ │ ├── login_locators.yaml│ │ ├── dashboard_locators.yaml│ │ └── settings_locators.yaml│ └── testdata/ # Test data files│ ├── login_testdata.xlsx│ └── users_testdata.json├── lib/ # Custom Python libraries│ ├── __init__.py│ └── custom_library.py└── executors/ # Run scripts ├── run_tests.bat └── run_tests.shTraditional Project Structure
robot-test-project/├── pyproject.toml├── uv.lock├── .gitignore├── README.md├── tests/ # Test suites (replaces features/steps)│ ├── login_tests.robot│ ├── dashboard_tests.robot│ └── settings_tests.robot├── resources/│ ├── keywords/│ │ ├── ui_keywords.robot│ │ └── api_keywords.robot│ ├── locators/│ │ ├── login_locators.yaml│ │ ├── dashboard_locators.yaml│ │ └── settings_locators.yaml│ └── testdata/│ ├── login_testdata.xlsx│ └── users_testdata.json├── lib/│ ├── __init__.py│ └── custom_library.py└── executors/ ├── run_tests.bat └── run_tests.shThe key difference is simple. BDD projects separate features from their step implementations, while traditional projects keep tests in a single tests directory. Everything else remains the same because the organizational principles apply to both approaches.
Environment-specific configuration lives in environment variables with clear, consistent naming. Secrets are never hardcoded. Your CI/CD pipeline runs automatically on commits, giving fast feedback through dry runs and targeted test execution.
This structure isn’t arbitrary. Every decision here is based on real-world experience with what makes test automation projects maintainable. When you need to update a locator, you know exactly where to find it. When you need to add a new test, you have clear patterns to follow. When someone new joins the team, they can navigate the project intuitively because it follows logical organizing principles.
What Success Looks Like
You’ll know your project structure is working when certain things become easy. Onboarding a new QA engineer should take hours, not days, because the project structure is self-documenting. Running tests on a new machine should be just a uv sync away. Debugging a failing test should lead you naturally to the right file because everything is where it logically should be.
Most importantly, you should be able to focus on writing good tests rather than fighting with your project setup. That’s the real goal here. A good project structure gets out of your way and lets you concentrate on what actually matters—making sure your application works correctly. The structure becomes invisible, and that’s exactly what you want.