Project Brief — Build a Test Data Management Utility

10 min read

You've spent eight chapters learning the parts. This chapter is where the parts come together. The capstone is a real, runnable Java application — not a toy snippet — that exercises classes, inheritance, interfaces, collections, file I/O, JSON parsing, exception handling, lambdas, and streams in one codebase. By the end you'll have something you can show to a hiring manager, paste on a CV, or hand to a junior who joins the team. This first lesson sets the scope and the structure; lesson 2 walks you through the implementation; lesson 3 is your self-review.

What you're building

QA DataManager — a command-line utility that manages test data fixtures for a QA team. The kind of utility every test framework needs once you scale beyond a handful of tests:

  • Reads JSON fixture files (users.json, products.json) and deserialises them into typed Java objects.
  • Processes the data — list all, filter by field, sort by field, search by ID.
  • Generates new randomised entries (with realistic-looking names and emails) for tests that need fresh data.
  • Writes the processed or generated data back out as JSON or CSV.
  • Logs every operation with a timestamp to an operations.log file.
  • Handles errors gracefully — missing files become a friendly error, malformed JSON triggers a domain-specific exception, invalid input is rejected loudly at the boundary.

This isn't fictional homework. Real QA teams build exactly this kind of tool. Test fixture management, test data generation for parameterised runs, sanitising prod-like data into fixture files — every shop has a handful of these scripts in some half-maintained corner of their repo. Yours will be cleaner.

Why this scope

The brief is deliberately chosen to force every concept from the course to do real work:

  • Classes & objects (Ch 4): TestUser, Product, plus a TestData interface and a DataManager abstract base class.
  • Inheritance & polymorphism (Ch 4 & 5): UserManager and ProductManager both extend DataManager. The Main class holds them as DataManager references and dispatches polymorphically.
  • Interfaces (Ch 5): TestData is the cross-cutting capability "this thing has an ID and a one-line summary."
  • Collections (Ch 6): List<TestUser> for fixtures, Map<String, String> for config, HashSet<String> to dedupe generated emails.
  • Exceptions & file I/O (Ch 7): try-with-resources for files, a custom TestDataException for fixture problems, IllegalArgumentException for input validation.
  • JSON (Ch 7): Jackson ObjectMapper for round-tripping JSON.
  • Strings, regex, streams (Ch 8): String.format for log lines, regex for email validation, stream pipelines for filtering and sorting.

If you can build the brief end-to-end, you've used every Java skill QA hiring managers ask about. That's the actual goal.

What success looks like

A working Main you can run from the command line:

$ mvn package
$ java -jar target/qa-datamanager-1.0.jar
 
QA DataManager pick an operation:
  1. List users
  2. Filter users by role
  3. Generate N random users
  4. Sort users by name
  5. Find user by id
  6. Export users to CSV
  7. List products
  8. Filter products by category
  9. Quit
 
> 1
[2026-05-06 09:30:01] LIST users (n=4)
1: Alice    (alice@test.com)    [admin]
2: Bob      (bob@y.com)         [member]
3: Carol    (carol@z.com)       [admin]
4: Dave     (dave@x.com)        [guest]

Every menu choice exercises a different code path. The output is human-readable. Errors don't crash — they print a clear message and re-show the menu. The JSON fixtures and the operations log live in data/ and output/ next to the jar.

Project structure

A standard Maven layout. Put each class in the package that matches its role:

qa-datamanager/
├── pom.xml                                  # Maven build, Jackson dep
├── data/
│   ├── users.json                           # input fixture
│   └── products.json                        # input fixture
├── output/
│   ├── operations.log                       # written by the Logger
│   ├── users.cleaned.json                   # generated/exported data
│   └── users.csv                            # CSV export
└── src/
    └── main/
        └── java/
            ├── Main.java                    # menu loop, wires managers together
            ├── model/
            │   ├── TestData.java            # interface — getId(), toSummary()
            │   ├── TestUser.java            # implements TestData
            │   └── Product.java             # implements TestData
            ├── manager/
            │   ├── DataManager.java         # abstract base
            │   ├── UserManager.java         # extends DataManager
            │   └── ProductManager.java      # extends DataManager
            ├── generator/
            │   └── DataGenerator.java       # random users / products
            ├── util/
            │   ├── FileHelper.java          # read/write JSON, ensure dirs
            │   └── Logger.java              # timestamped operations.log
            └── exception/
                └── TestDataException.java   # custom unchecked exception

Don't get hung up on the package depth — flatten it if you want, or keep Main at the root. The point is to separate models, managers, utilities, and infrastructure code so each file has one clear job.

Sample fixtures

Start with these. Save them in data/ and check in any time you make changes you want to keep.

data/users.json:

[
  { "id": "u1", "name": "Alice", "email": "alice@test.com", "role": "admin",  "active": true  },
  { "id": "u2", "name": "Bob",   "email": "bob@y.com",      "role": "member", "active": true  },
  { "id": "u3", "name": "Carol", "email": "carol@z.com",    "role": "admin",  "active": false },
  { "id": "u4", "name": "Dave",  "email": "dave@x.com",     "role": "guest",  "active": true  }
]

data/products.json:

[
  { "id": "p1", "name": "Pro Plan",     "category": "subscription", "priceCents": 4900 },
  { "id": "p2", "name": "Team Add-on",  "category": "addon",        "priceCents": 1500 },
  { "id": "p3", "name": "Free Plan",    "category": "subscription", "priceCents": 0    },
  { "id": "p4", "name": "Support Pack", "category": "service",      "priceCents": 9900 }
]

The structures are intentionally simple — enough fields to make filtering and sorting interesting, not so many that you'll spend half the project on getter/setter ceremony.

Operations to support

A non-exhaustive list of the operations your menu should drive. Build the easy ones first; some of these turn into stretch goals in lesson 3.

#OperationSkills exercised
1List users / productsbasic iteration, polymorphism via DataManager.listAll()
2Filter by role / categoryPredicate<T>, Stream .filter
3Sort by name / priceComparator, Stream .sorted
4Find by IDStream .findFirst(), Optional
5Generate N random usersDataGenerator, HashSet for unique emails
6Export to CSVStringBuilder, String.join, FileWriter
7Save filtered fixture as JSONObjectMapper.writerWithDefaultPrettyPrinter()
8Show operation log tailBufferedReader line streaming

Each row maps to one or two lessons from earlier in the course. If you find yourself stuck on row N, that's the chapter to revisit before lesson 2 of this capstone.

The end-to-end flow

That's the whole utility on one page. Read the arrows left to right: input JSON in, manager loads it, stream pipeline transforms it, output writer drops the result on disk, logger records every operation. Nothing more, nothing less.

Setup checklist before you start lesson 2

Before you open lesson 2, get the project skeleton ready. Don't write any logic yet — that's lesson 2's job. Just lay the rails:

  1. Create the folder. mkdir qa-datamanager && cd qa-datamanager. Inside, create data/ and output/ folders, plus the standard src/main/java/... Maven layout.
  2. Add a pom.xml with a groupId, artifactId (qa-datamanager), version (1.0), Java 17 or 21 in <maven.compiler.source> and <target>, and the Jackson jackson-databind dependency from chapter 7 lesson 4.
  3. Open the project in IntelliJ. File → Open → pom.xml. Confirm IntelliJ resolves Jackson — the import in a stub file should turn green.
  4. Drop in the sample fixtures. Paste users.json and products.json from above into the data/ folder.
  5. Stub out empty classes in the right packages — even just class TestUser {}. This lets you see the structure before you fill in the bodies and gives IntelliJ something to reason about for autocompletion.
  6. Confirm mvn package builds an empty jar. It will say nothing was compiled, which is fine — you've validated the pipeline runs.

The setup itself is a useful 30-minute exercise. If mvn package fails, that's a Maven or JDK problem to fix before you start writing logic. Lesson 2 assumes the build works.

Project work

Spend 30-45 minutes laying the foundation:

  1. Set up the Maven project as above (folder layout, pom.xml with Jackson).
  2. Drop in both users.json and products.json exactly as shown.
  3. Create empty stubs for every file in the structure tree — TestUser.java, TestData.java, DataManager.java, etc. Each can be a one-line public class X {} for now.
  4. Verify the project compiles: mvn package (or build via IntelliJ). Fix any path or pom issues now, with stubs, before you've written real logic.
  5. Open users.json in the JSON Formatter on qa.codes to confirm it's valid. Do the same for products.json. Treat valid JSON as a precondition; debugging Jackson errors with malformed input is much harder than fixing the JSON first.
  6. Sketch the DataManager abstraction on paper or in a comment in DataManager.java: which methods should be abstract? Which concrete? Which should be final? Lesson 2 will give you the answer; trying it yourself first makes the answer stick.
  7. Open the empty Main.java and write — as a comment, not code — the menu items you'll support. Your menu doesn't have to match the table above exactly; it has to make sense to you.

When the empty project compiles, the JSON validates, and your design notes feel right, you're ready for lesson 2 — the implementation walkthrough.

// tip to track lessons you complete and pick up where you left off across devices.