Test code reads files (CSV fixtures, JSON configs, log inputs) and writes files (reports, screenshots metadata, exported runs). Java's file I/O has two halves: the modern java.nio.file.Files helpers — short, easy, ideal for small files — and the classic streaming readers and writers — BufferedReader, FileWriter, PrintWriter — for large files and line-by-line processing. Both halves require try-with-resources to close their handles cleanly, which is the most important pattern in this lesson.
try-with-resources — the close-cleanup baseline
A file handle is a resource that the operating system holds on your behalf. Forgetting to close it leaks file descriptors, which can hang the JVM under load. Java's solution is try (Resource r = ...) — the resource is closed automatically when the block exits, success or failure:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesShape {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("config.json"))) {
// use reader here
} catch (IOException e) {
System.out.println("read failed: " + e.getMessage());
}
// reader is closed automatically — no finally needed
}
}Anything that implements AutoCloseable works: file handles, network sockets, database connections, JDBC ResultSet, Selenium WebDriver. Always prefer try (...) over a manual finally { reader.close(); }. It's safer (close exceptions are handled correctly), shorter, and harder to get wrong.
Reading a file line by line — BufferedReader
The classic approach for files of unknown size:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class CountFailures {
public static void main(String[] args) {
int failures = 0;
try (BufferedReader reader = new BufferedReader(new FileReader("test-results.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("FAILED")) failures++;
}
} catch (IOException e) {
System.out.println("error reading: " + e.getMessage());
return;
}
System.out.println("Failures found: " + failures);
}
}A typical input test-results.txt:
PASS Login
PASS Search
FAILED Checkout
PASS Logout
FAILED Export
Output:
Failures found: 2
The pattern while ((line = reader.readLine()) != null) is so common it's worth memorising. readLine() returns the next line (without its trailing newline) or null at end of file. The whole loop streams the file — only one line lives in memory at a time. That's what makes BufferedReader the right tool for big files, log scans, and any data you want to process line-by-line.
Reading a whole file in one call — Files.readString (Java 11+)
For small config and fixture files, the modern shortcut is one line:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadAll {
public static void main(String[] args) throws IOException {
String content = Files.readString(Path.of("config.json"));
System.out.println(content);
}
}Files.readString reads the entire file into a String and returns it. Convenient for files up to a few megabytes; do not use this on a 200 MB log — it loads everything into memory. The streaming BufferedReader is the right tool there.
The sibling Files.readAllLines(Path.of(...)) reads into a List<String> — useful when you want the lines in memory for indexing or sorting:
List<String> lines = Files.readAllLines(Path.of("test-results.txt"));
System.out.println("first line: " + lines.get(0));Path.of(...) (Java 11+) builds a Path from a string. The older Paths.get(...) does the same thing; both are fine. We'll use Path.of.
Writing a file — FileWriter
import java.io.FileWriter;
import java.io.IOException;
public class WriteReport {
public static void main(String[] args) {
try (FileWriter writer = new FileWriter("report.txt")) {
writer.write("Test: Login\n");
writer.write("Status: PASSED\n");
writer.write("Duration: 1250ms\n");
} catch (IOException e) {
System.out.println("write failed: " + e.getMessage());
}
System.out.println("Wrote report.txt");
}
}new FileWriter("report.txt") opens the file for writing, replacing anything that was there. After the try block closes, the writer flushes and closes the file.
If you want to append instead of replace, pass true as the second argument:
try (FileWriter log = new FileWriter("run.log", true)) {
log.write("[2026-05-06 09:00] suite started\n");
}Each call writes a chunk; you're responsible for newline characters (\n).
Writing with formatting — PrintWriter
FileWriter.write(...) only takes Strings. PrintWriter wraps it and adds the same println(...) and printf(...) you've used on System.out:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class FormattedReport {
public static void main(String[] args) {
int passed = 28, failed = 2;
long durationMs = 12450;
try (PrintWriter writer = new PrintWriter(new FileWriter("summary.txt"))) {
writer.println("=== Test Run Summary ===");
writer.printf("Passed: %d%n", passed);
writer.printf("Failed: %d%n", failed);
writer.printf("Total time: %.2fs%n", durationMs / 1000.0);
writer.println("========================");
} catch (IOException e) {
System.out.println("write failed: " + e.getMessage());
}
System.out.println("Wrote summary.txt");
}
}%n is the platform-correct newline; on Windows it's \r\n, on macOS/Linux it's \n. Use %n rather than \n in printf so the file is correctly formatted on every OS.
Writing all-in-one — Files.writeString
The Java 11+ counterpart to readString:
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
public class WriteOnce {
public static void main(String[] args) throws IOException {
Files.writeString(Path.of("hello.txt"), "Hello, QA!\nLine 2\n");
System.out.println("done");
}
}One line, no streams. Perfect when you've already built the entire file content in memory (as a String) and just need to drop it on disk. Add StandardOpenOption.APPEND as a third argument to append rather than replace.
A round trip — read, transform, write
Putting reading, writing, and the rest of the chapter together — drop every line marked SKIP_ from a fixture file:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
public class Stripper {
public static void main(String[] args) throws IOException {
Path in = Path.of("input.txt");
Path out = Path.of("output.txt");
List<String> lines = Files.readAllLines(in);
List<String> kept = lines.stream()
.filter(l -> !l.startsWith("SKIP_"))
.collect(Collectors.toList());
Files.write(out, kept);
System.out.println("kept " + kept.size() + " of " + lines.size() + " lines");
}
}For an input.txt of:
login
SKIP_legacy
checkout
SKIP_export
logout
Output:
kept 3 of 5 lines
Files.write(path, lines) writes a List<String> as a UTF-8 file with a newline after each line — the perfect counterpart to readAllLines.
A read flow, step by step
Step 1 of 6
Open the file
new BufferedReader(new FileReader("results.txt")) — the JVM asks the OS for a file handle.
That six-step cycle covers BufferedReader reads, FileWriter writes, and try-with-resources for both. Each step in the diagram is one or two lines of Java; understanding the order is what keeps your file code from leaking handles.
⚠️ Common mistakes
- Forgetting try-with-resources. A
BufferedReadernot insidetry (...)won't close on exception. The OS keeps the handle around until garbage collection — and "until garbage collection" can be a long time on a busy CI agent. Always use try-with-resources unless there's a specific reason not to. - Reading a 1 GB file with
Files.readString. It loads the whole file into memory and OOMs on big logs. For anything above a few MB, stream withBufferedReader.readLine(). TreatreadString/readAllLinesas small-file conveniences. - Mixing
\nand%ninprintf. On Windows,\nis just LF; the platform expects CRLF.%nalways does the right thing. Use%ninsideprintf/formatstrings; reserve\nfor hard-coded raw byte writes.
🎯 Practice task
Read, transform, and write a fixture file. 25-30 minutes.
- Create a text file
users.txtnext to your Java code with five lines, e.g.alice@x.com,admin,bob@y.com,member,SKIP_carol@z.com,guest,dave@x.com,member,SKIP_eve@y.com,member. - Create
UserCleaner.java. UseFiles.readAllLines(Path.of("users.txt"))to load every line. - Filter out anything that starts with
SKIP_using a Stream pipeline —lines.stream().filter(l -> !l.startsWith("SKIP_")).toList(). - Write the survivors to
users.cleaned.txtusingFiles.write(Path.of("users.cleaned.txt"), kept). - Open
users.cleaned.txtin your editor and confirm it contains only the non-skipped lines. - Now do the same job with
BufferedReaderandPrintWriter(line-by-line streaming): open both inside the same try-with-resources (try (BufferedReader br = ...; PrintWriter pw = ...)),readLine()in a loop, andpw.println(line)for survivors. Confirm the output is identical. - Add a deliberate
throw new IOException("oops")inside the streaming version's loop. Confirm the exception still triggersclose()on both resources — that's try-with-resources doing its job. - Stretch: add a third file,
users.skipped.txt, that contains exactly the lines you filtered out. You'll have twoPrintWriters in the same try-with-resources. Notice that try-with-resources accepts multiple semicolon-separated resources; both are closed regardless of which throws first.
You can now move data between Java and the file system. Lesson 4 introduces JSON parsing — the format every API response and fixture you'll meet is written in.