Strings are the most common type in test code — URLs, selectors, response bodies, log messages, status labels. Java's String class is rich and immutable: every operation returns a new String rather than modifying the original. That immutability is why == comparisons fail (lesson 1.2) and why string-concatenation in a loop is a performance trap. This lesson covers the methods you'll use daily, the formatting helpers that replace ugly + chains, and StringBuilder — the mutable companion that fixes the loop performance problem.
Strings are immutable
Every method that "modifies" a String actually returns a new one:
String s = "hello";
s.toUpperCase(); // returns a new "HELLO" — but we threw it away
System.out.println(s); // still "hello"
s = s.toUpperCase(); // capture the result
System.out.println(s); // "HELLO"If you don't reassign, the result is discarded. This is a frequent beginner bug — calling s.trim() on a line and wondering why the whitespace is still there. The methods are non-mutating; you must capture their return values.
The everyday methods
public class StringBasics {
public static void main(String[] args) {
String url = "https://staging.myapp.com/dashboard";
String email = " Alice@TEST.com ";
String csv = "Chrome,Firefox,Safari,Edge";
// length — number of characters
System.out.println("len: " + url.length());
// contains / startsWith / endsWith — substring queries
System.out.println(url.contains("dashboard"));
System.out.println(url.startsWith("https"));
System.out.println(url.endsWith(".com/dashboard"));
// substring — slice
System.out.println(url.substring(8)); // staging.myapp.com/dashboard
System.out.println(url.substring(8, 25)); // staging.myapp.com
// trim / strip — clean whitespace
System.out.println("[" + email.trim() + "]");
System.out.println("[" + email.strip() + "]"); // Java 11+, Unicode-aware
// case
System.out.println(email.toLowerCase().trim());
// replace — exact substring
System.out.println(url.replace("staging", "production"));
// split — array of pieces
String[] browsers = csv.split(",");
System.out.println("first: " + browsers[0] + ", count: " + browsers.length);
// join — collection back to string
System.out.println(String.join(" | ", browsers));
// isEmpty / isBlank
System.out.println("\"\".isEmpty(): " + "".isEmpty());
System.out.println("\" \".isBlank(): " + " ".isBlank());
}
}Output:
len: 36
true
true
true
staging.myapp.com/dashboard
staging.myapp.com
[Alice@TEST.com]
[Alice@TEST.com]
alice@test.com
https://production.myapp.com/dashboard
first: Chrome, count: 4
Chrome | Firefox | Safari | Edge
"".isEmpty(): true
" ".isBlank(): true
A few callouts:
equals/equalsIgnoreCase— content equality. Always over==for Strings (chapter 2.1).trim()vsstrip()—trimremoves ASCII whitespace;strip(Java 11+) is Unicode-aware. Preferstripin modern code.isEmpty()vsisBlank()—isEmptyis true only for"";isBlankis true for""," ","\t\n". For "user typed nothing meaningful,"isBlank.substring(start)vssubstring(start, end)— both with zero-based indices, the second is exclusive.substring(8, 25)returns characters 8 through 24.split(regex)— note the argument is a regex (lesson 2). For literal splits, single characters work as expected; for.or|you need to escape (split("\\.")).
String.format — printf-style formatting
Building strings with + gets ugly:
String name = "Login";
String status = "PASSED";
long ms = 1450;
String line = name + ": " + status + " (" + ms + "ms)";String.format does the same with placeholders:
String line = String.format("%s: %s (%dms)", name, status, ms);
System.out.println(line);Output:
Login: PASSED (1450ms)
The conversions you'll use most:
%s— String (calls.toString()on whatever you pass)%d— integer%f/%.2f— float/double, optionally with precision%n— platform newline (use this, not\n, in formatted output)%-20s— left-aligned, padded to 20 columns
System.out.printf(...) writes the formatted string straight to stdout without the intermediate variable.
A real example — a fixed-width test report row:
System.out.printf("%-20s | %-10s | %6dms%n", "checkout-suite", "FAILED", 3120);
System.out.printf("%-20s | %-10s | %6dms%n", "login", "PASSED", 1450);Output:
checkout-suite | FAILED | 3120ms
login | PASSED | 1450ms
The columns line up because every cell is padded to a fixed width. The same trick is what makes JUnit and TestNG console output readable.
StringBuilder — concatenation in loops
Strings are immutable. Each + allocates a new String and copies the characters:
String report = "";
for (TestResult r : results) {
report += r.name + ": " + r.status + "\n"; // ❌ allocates O(n²) bytes total
}Three iterations are fine. A thousand iterations is measurably slow. StringBuilder is a mutable buffer that appends in place; you call .toString() once at the end:
StringBuilder report = new StringBuilder();
for (TestResult r : results) {
report.append(r.name)
.append(": ")
.append(r.status)
.append("\n");
}
String finalReport = report.toString();append returns the same StringBuilder, so you can chain calls. The whole loop is O(n) instead of O(n²). Use StringBuilder whenever you build a string in a loop.
A working example:
public class ReportBuilder {
static class TestResult {
String name; String status; long durationMs;
TestResult(String n, String s, long d) { name = n; status = s; durationMs = d; }
}
public static void main(String[] args) {
TestResult[] results = {
new TestResult("Login", "PASSED", 1450),
new TestResult("Search", "PASSED", 820),
new TestResult("Checkout", "FAILED", 3120),
new TestResult("Logout", "PASSED", 990)
};
StringBuilder sb = new StringBuilder();
sb.append("=== Run Summary ===").append(System.lineSeparator());
for (TestResult r : results) {
sb.append(String.format("%-12s %-8s %6dms%n", r.name, r.status, r.durationMs));
}
sb.append("===================");
System.out.println(sb.toString());
}
}Output:
=== Run Summary ===
Login PASSED 1450ms
Search PASSED 820ms
Checkout FAILED 3120ms
Logout PASSED 990ms
===================
StringBuilder here pulls double duty — efficient concatenation and a convenient place to compose a multi-line report before printing it.
Cleaning API response strings — a real QA case
Whitespace, casing, and stray characters in API or DOM-extracted text catch tests that look right at a glance:
public class CompareSafely {
public static void main(String[] args) {
String fromApi = " Alice@TEST.com \n"; // copied from a JSON field
String expected = "alice@test.com";
boolean naive = fromApi.equals(expected); // false
boolean clean = fromApi.strip().toLowerCase().equals(expected);
System.out.println("naive: " + naive);
System.out.println("clean: " + clean);
}
}Output:
naive: false
clean: true
Reaching for strip().toLowerCase() (or equalsIgnoreCase) before comparing values pulled from external sources is a habit worth building early. It's the difference between flaky tests that pass on Mondays and tests that mean something.
A field guide to String methods
String methods organised by intent
| Method | Example | |
|---|---|---|
| Compare | equals, equalsIgnoreCase, contains, startsWith, endsWith, isEmpty, isBlank | url.startsWith("https"); body.contains("error"); s.isBlank(); |
| Extract | length, charAt, substring, indexOf, lastIndexOf, split | url.substring(8); csv.split(","); s.indexOf("@"); |
| Transform | trim, strip, toLowerCase, toUpperCase, replace, replaceAll, repeat | s.strip().toLowerCase(); url.replace("staging","prod"); |
| Build | concat (+), String.join, String.format, StringBuilder.append | String.join(", ", browsers); String.format("%s: %dms", n, ms); |
The four buckets cover almost everything you'll do to a String in test code. Memorise the bucket — when you need a method, you can usually guess the name; if you can't, your IDE's autocomplete will surface it.
⚠️ Common mistakes
- Calling a String method and ignoring the return value.
s.trim();does nothing useful — the cleaned String is thrown away. Reassign or chain:s = s.trim();orif (s.trim().equals("OK")) .... Strings are immutable; the methods can't changesin place. s == "expected"for content comparison. Reviewed twice already because it's the most common Java bug..equals()(or.equalsIgnoreCase()) for content.==for primitives only.- Building large strings with
+=in a loop. O(n²) work; the JVM cannot optimise this away. UseStringBuilder.append(...)and call.toString()once at the end. The single-line+outside loops is fine — the compiler optimises individual concatenations into oneStringBuildercall.
🎯 Practice task
Build a formatted test report. 25-30 minutes.
- Create
RunReport.java. Define a small static classResult { String name; String status; long durationMs; }with a constructor. - In
main, buildResult[] results = { ... }with at least five mixed pass/fail results. - Use a
StringBuilderto compose the full report. Add a header line=== Run ===, then one line per result formatted withString.format("%-15s %-8s %6dms%n", ...). UseSystem.lineSeparator()for the header newline. - Compute three summary numbers from the array: total, passed (use a counter), and average duration. Append them to the
StringBuilderas a footer. - Print the full report once with
System.out.println(sb.toString()). - Add
Resultentries with leading/trailing whitespace innameand uppercasestatus(e.g." Login ","PaSseD"). Inside the loop,strip()the name andtoUpperCase()the status before formatting. Confirm the report is consistent. - Stretch: convert the loop to use
String.join(System.lineSeparator(), lines)wherelinesis aList<String>you build inside the loop. Compare readability. For a small number of lines,String.joinis often clearer than aStringBuilder.
You can now compose any string a test framework needs. Lesson 2 introduces regular expressions — the next layer up, when "starts with" and "contains" aren't enough.