Review and Stretch Goals

8 min read

You've built QA DataManager — model classes, an abstract base manager with two concrete subclasses, a generator, a logger, file utilities, a menu loop, and a custom exception. That's a real Java project. This lesson is your self-review (so you know where the gaps are) and your map for what comes next (so you know which courses to take to convert this foundation into employability). Take the checklist seriously: ticking a box you don't actually understand is the difference between knowing Java and knowing about Java, and only the first one will land an automation role.

Self-assessment checklist

Read each item and answer honestly: yes, partly, no. Where you're not at yes, the chapter or lesson to revisit is named.

OOP foundations (Chapters 4–5):

  • I can explain why TestUser and Product both implement TestData rather than extending a common parent. (Ch 5.4)
  • I can explain why DataManager is abstract instead of a regular class. (Ch 5.1)
  • I can explain what <T extends TestData> means and why it lets UserManager work without casting. (Ch 5 — generics, plus Ch 4.4 inheritance)
  • I can describe what happens when ProductManager.loadFrom(...) runs and why the JVM picks the right override even when the variable is typed DataManager<?>. (Ch 5.3 — runtime polymorphism)
  • I have at least one protected field and I can articulate why it isn't public or private. (Ch 4.3)
  • I used @Override on every method that overrides a parent method or implements an interface method. (Ch 4.5)

Collections and iteration (Chapter 6):

  • I can explain why DataManager.items is a List<T> (the interface) and not an ArrayList<T> (the implementation). (Ch 6.1)
  • I used a HashSet<String> to deduplicate generated emails and I can explain why a set is faster than list.contains(...) for that check. (Ch 6.3)
  • My filtering and sorting code uses Streams (.stream().filter(...).sorted().toList()) rather than imperative loops. (Ch 8.4)
  • I can write the same operation in three different forms (for-each, removeIf, Stream) and articulate when each is appropriate. (Ch 6.4)

File I/O and JSON (Chapter 7):

  • All my file operations use try-with-resources (or higher-level helpers like Files.writeString). (Ch 7.3)
  • My loadFrom(...) catches IOException and re-throws as TestDataException with the original as the cause. (Ch 7.2)
  • My Main catches TestDataException specifically — not Exception, not Throwable. (Ch 7.1)
  • My JSON round-trip (read → modify → write) preserves field order via LinkedHashMap or the model class's field order. (Ch 6.2 / Ch 7.4)
  • My CSV export uses StringBuilder rather than += in the loop. (Ch 8.1)

Strings and modern Java (Chapter 8):

  • Every String comparison in my code uses .equals(...) or .equalsIgnoreCase(...), never ==. (Ch 2.2)
  • My log timestamps use String.format or printf rather than concatenation chains. (Ch 8.1)
  • At least one of my methods accepts a lambda — directly or via Comparator, Predicate, or Function. (Ch 8.3)
  • My streams use a method reference (User::getName) at least once. (Ch 8.3)
  • I can explain what Optional is and how findFirst().orElse(...) differs from returning null. (Ch 8.4)

Error handling:

  • My code refuses invalid input at the boundary (e.g. IllegalArgumentException for n <= 0 in the generator). (Ch 7.1, 4.3)
  • My Main menu loop continues running after a single operation throws — only Quit exits. (Ch 7.1)
  • My logger doesn't throw on failure; it falls back to System.err. (project-specific, Ch 7.1)

If you're at yes for at least three-quarters of these, you've internalised the core. The partly and no answers tell you precisely which lessons to re-read.

Reflection — the harder questions

Concepts can be ticked off mechanically. Design intuition is built by asking yourself why you made each choice. For each, write one or two sentences in your project README:

  1. Why is DataManager abstract instead of an interface? Hint: it has fields and a constructor.
  2. Why is TestData an interface instead of an abstract class? Hint: would a fourth model class (e.g. Order) be likely to extend TestData and something else?
  3. What happens if I make DataManager.listAll() final? When would I want to? Hint: do you trust subclasses with the lifecycle?
  4. Why does loadFrom wrap IOException instead of letting it propagate? Hint: which layer should know about Jackson and which shouldn't?
  5. If a fifth model type (Order) appears tomorrow, how many files do I have to add or change? Three (the model, the manager, the menu wiring) is a healthy answer; ten is not.

Working through these questions is what separates "passed the course" from "can defend the architecture in a code review." Save the answers somewhere — you'll re-read them when interview prep season hits.

Stretch goals — five real upgrades

The capstone as written is a complete project. Each of these turns it into a portfolio-grade one:

  1. JUnit 5 tests for DataGenerator and UserManager. Add the JUnit 5 dependency, write tests that confirm:
    • generateUsers(100) returns 100 unique emails.
    • generateUsers(0) throws IllegalArgumentException.
    • loadFrom("missing.json") throws TestDataException with a non-null cause. This is the most valuable upgrade — the moment you're writing tests for your test utility, you're doing the QA Engineer's actual job.
  2. CSV export with proper quoting. Names with commas in them break the simple String.join(",", ...) shape. Quote any field that contains a comma or a newline ("\"" + value.replace("\"", "\"\"") + "\""). Or pull in OpenCSV if you'd rather use a library — both are valid choices to defend.
  3. Validation in setters. Move email-shape and price checks into TestUser.setEmail and Product.setPriceCents. Use a regex from Ch 8.2 for the email. Throw IllegalArgumentException with a useful message. Now the type system prevents bad data from existing inside your program at all.
  4. config.json for paths and settings. Move the hard-coded "data/users.json", "output/users.csv", etc. into a small config.json parsed once at startup with ObjectMapper. The menu then asks the manager for paths, not hard-coded strings. This is the shape every real test framework eventually grows into.
  5. Multi-field sort with Comparator.thenComparing. Sort users by role first, name second:
    users.stream()
        .sorted(Comparator.comparing(TestUser::getRole)
                          .thenComparing(TestUser::getName))
        .toList();
    Three lines, multi-key sort. Shows in the diff how Java has caught up to functional languages for data work.

Each stretch goal is a chunk of one to two hours. Do at least two before moving on.

Where to go next

Core Java is a foundation, not a destination. The path from "I can write Java" to "I'm a Java automation engineer" runs through the four pillars:

Core Java for QA — done
  • – Selenium with Java
  • – Page Object Model — uses every concept here
  • – Selenide for higher-level Selenium
  • – Rest Assured
  • – JSON Schema validation
  • – Contract testing — Pact
  • – TestNG — annotations, parallel runs, listeners
  • – JUnit 5 — extensions, parameterised tests
  • – Cucumber-JVM — BDD on the JVM
  • Maven & Gradle properly –
  • Jenkins, GitHub Actions for Java –
  • Allure / ExtentReports for output –

A practical 90-day plan if you want one:

  • Month 1 — Selenium + JUnit/TestNG. Pick one runner; build a small five-page Page Object framework on top of your capstone's structure. The BasePage extends DataManager design from Ch 4 is intentionally close to the Selenium POM you'll build.
  • Month 2 — Rest Assured. Write API tests for any free public API (the JSONPlaceholder endpoints are perfect). You'll reuse exactly the JSON, exception, and stream code you've already written.
  • Month 3 — CI integration & reporting. Wire your project to GitHub Actions (the CI/CD for Testers cheatsheet on qa.codes is a useful starting point). Add Allure for HTML reports.

By the end you'll have a public GitHub repo running real tests against real services on real CI. That's the artefact hiring managers want to see, and it's a genuine year of skills compressed into a focused quarter.

Project work — final polish

Spend 60-90 minutes wrapping up:

  1. Walk every checklist item above. For any partly or no, return to the cited lesson, fix the gap, and re-tick.
  2. Pick two stretch goals and implement them. Resist the urge to do all five — depth beats breadth at this stage.
  3. Write the README answers to the five reflection questions. Aim for one or two sentences per question; if a question takes a paragraph, you probably haven't decided yet — that's where to think more.
  4. Run mvn package. Run the resulting jar. Confirm every menu item works on a fresh checkout (delete output/, then run — it should recreate the directory).
  5. Open the project on a friend's machine if you can — the moment your "works on my machine" project runs on someone else's is the moment it's actually portable.
  6. Commit and push to a public GitHub repo. Tag the README with the courses you completed (Core Java for QA) and a one-paragraph "what I learned" section. Recruiters skim READMEs.
  7. Pick one downstream course from the ConceptMap above and start it tomorrow. Momentum matters.

That's the end of Core Java for QA. Eight chapters, thirty-six lessons, one real Java project on disk that exercises every concept. The next step — Selenium, Rest Assured, TestNG — uses every line of what you've just built. See you in the next course.

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