Bagian penutup ini menjawab dua kebutuhan terakhir proyek: jaminan perilaku inti tetap benar dan bentuk akhir yang bisa dibagikan. Pengujian memakai harness sederhana berbasis method main — mencetak PASS/FAIL per pemeriksaan dan keluar dengan exit code bermakna — karena lingkungan kerja proyek ini belum menyertakan dependency JUnit. Setelah lulus, seluruh berkas dikompilasi, dirakit menjadi tugas.jar, dan dijalankan sebagai aplikasi lengkap.
Strategi Pengujian Tanpa Pustaka Eksternal #
Pengujian berarti menjalankan potongan perilaku secara terpisah dari menu, lalu membandingkan hasil dengan nilai yang diharapkan. DaftarTugas dan PenyimpananTugas mendukung hal tersebut sejak awal: keduanya tidak membaca System.in, dan lokasi berkas disuntik lewat constructor sehingga pengujian memakai berkas sementara, bukan tugas.txt milik pengguna.
Harness buatan sendiri memenuhi tiga sifat dasar pengujian: setiap pemeriksaan bernama, hasil dicetak jelas, dan kegagalan mengubah exit code menjadi 1. Exit code inilah sinyal yang dibaca pipeline otomatis: pengujian gagal berarti build gagal.
Harness Pengujian Berbasis main #
Seluruh pemeriksaan dirangkum dalam UjiDaftarTugas:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Pengujian perilaku inti aplikasi tanpa pustaka eksternal.
* Setiap pemeriksaan mencetak PASS atau FAIL; exit code 0 berarti semua lulus.
*/
public class UjiDaftarTugas {
private static int gagal = 0;
public static void main(String[] args) throws Exception {
ujiTambah();
ujiValidasiJudul();
ujiTandaiSelesai();
ujiSimpanMuat();
ujiBerkasRusak();
System.out.println();
System.out.println("Pengujian selesai; jumlah gagal: " + gagal + ".");
if (gagal > 0) {
System.exit(1);
}
}
private static void ujiTambah() throws MasukanInvalidException {
DaftarTugas daftar = new DaftarTugas();
Tugas tugas = daftar.tambah(" Belajar Java dasar ");
periksa("tambah mengembalikan id pertama 1", tugas.id() == 1);
periksa("tambah memangkas spasi judul",
tugas.judul().equals("Belajar Java dasar"));
periksa("tugas baru belum selesai", !tugas.selesai());
Tugas kedua = daftar.tambah("Baca dokumentasi Files");
periksa("tugas kedua mendapat id 2", kedua.id() == 2);
}
private static void ujiValidasiJudul() {
periksa("judul kosong ditolak", ditolak(" "));
periksa("judul 101 karakter ditolak", ditolak("x".repeat(101)));
periksa("judul memuat karakter | ditolak", ditolak("Beli|Pulpen"));
}
private static void ujiTandaiSelesai()
throws MasukanInvalidException, TugasTidakDitemukanException {
DaftarTugas daftar = new DaftarTugas();
daftar.tambah("Belajar Java dasar");
periksa("id bukan angka ditolak",
lempar(() -> daftar.tandaiSelesai("abc"),
MasukanInvalidException.class));
periksa("id tidak ada ditolak",
lempar(() -> daftar.tandaiSelesai("9"),
TugasTidakDitemukanException.class));
Tugas tugas = daftar.tandaiSelesai("1");
periksa("tugas bisa ditandai selesai", tugas.selesai());
periksa("format baris selesai benar",
tugas.formatBaris().equals("[x] 1. Belajar Java dasar"));
}
private static void ujiSimpanMuat() throws Exception {
Path berkas = Path.of("uji-tugas.txt");
DaftarTugas daftar = new DaftarTugas();
daftar.tambah("Belajar Java dasar");
daftar.tambah("Baca dokumentasi Files");
daftar.tandaiSelesai("2");
PenyimpananTugas penyimpanan = new PenyimpananTugas(berkas);
penyimpanan.simpan(daftar);
periksa("berkas tersimpan", Files.exists(berkas));
periksa("baris pertama berkas benar",
Files.readAllLines(berkas, StandardCharsets.UTF_8).get(0)
.equals("1|Belajar Java dasar|aktif"));
DaftarTugas hasil = penyimpanan.muat();
periksa("jumlah tugas termuat kembali",
hasil.semua().size() == 2);
periksa("judul termuat kembali",
hasil.semua().get(0).judul().equals("Belajar Java dasar"));
periksa("status selesai termuat kembali",
hasil.semua().get(1).selesai());
}
private static void ujiBerkasRusak() throws IOException {
Path berkas = Path.of("uji-rusak.txt");
Files.writeString(berkas, "baris tanpa pemisah\n", StandardCharsets.UTF_8);
PenyimpananTugas penyimpanan = new PenyimpananTugas(berkas);
periksa("baris rusak memicu IOException",
lempar(penyimpanan::muat, IOException.class));
}
/** Mencetak hasil satu pemeriksaan dan menghitung kegagalan. */
private static void periksa(String nama, boolean kondisi) {
if (kondisi) {
System.out.println("PASS " + nama);
} else {
System.out.println("FAIL " + nama);
gagal++;
}
}
/** Menjalankan tambah; true bila judul ditolak oleh validasi. */
private static boolean ditolak(String judul) {
try {
new DaftarTugas().tambah(judul);
return false;
} catch (MasukanInvalidException e) {
return true;
}
}
/** Menjalankan perintah; true bila melempar exception bertipe persis. */
private static boolean lempar(Perintah perintah,
Class<? extends Exception> jenis) {
try {
perintah.jalankan();
return false;
} catch (Exception e) {
return e.getClass() == jenis;
}
}
/** Antarmuka satu method agar blok uji bisa melempar exception terperiksa. */
interface Perintah {
void jalankan() throws Exception;
}
}Output yang Dihasilkan:
PASS tambah mengembalikan id pertama 1
PASS tambah memangkas spasi judul
PASS tugas baru belum selesai
PASS tugas kedua mendapat id 2
PASS judul kosong ditolak
PASS judul 101 karakter ditolak
PASS judul memuat karakter | ditolak
PASS id bukan angka ditolak
PASS id tidak ada ditolak
PASS tugas bisa ditandai selesai
PASS format baris selesai benar
PASS berkas tersimpan
PASS baris pertama berkas benar
PASS jumlah tugas termuat kembali
PASS judul termuat kembali
PASS status selesai termuat kembali
PASS baris rusak memicu IOException
Pengujian selesai; jumlah gagal: 0.Mari kita bedah kerangka harness-nya:
- Field
gagaldan methodperiksa(...): satu tempat pencatatan kegagalan dan pencetakan hasil; memperluas harness cukup mengubah satu method, bukan tiap pemeriksaan. - Blok penutup
main: ringkasan dicetak laluSystem.exit(1)dipanggil bila ada kegagalan — exit code menjadi sinyal lolos atau tidak bagi pipeline CI. - Method
lempar(Perintah, Class): padanan sederhana pengujian “harus melempar”. Perbandingane.getClass() == jenismembuat exception salah tipe terhitung gagal. - Antarmuka
Perintah: antarmuka satu method mengizinkan lambda seperti() -> daftar.tandaiSelesai("abc")melempar exception terperiksa —Runnabletidak mengizinkan. - Method
ujiSimpanMuat(): memakai berkas sementarauji-tugas.txt, bukantugas.txtmilik pengguna — manfaat penyuntikan constructor dari Bagian 3.
Padanan Pengujian dengan JUnit 5 #
Dengan JUnit 5, kelompok pemeriksaan yang sama menjadi method beranotasi @Test dan perbandingan nilai memakai assertion:
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UjiDaftarTugasJUnit {
@Test
void tambahMenetapkanIdBerurutan() throws MasukanInvalidException {
DaftarTugas daftar = new DaftarTugas();
Tugas tugas = daftar.tambah(" Belajar Java dasar ");
assertEquals(1, tugas.id());
assertEquals("Belajar Java dasar", tugas.judul());
assertFalse(tugas.selesai());
}
@Test
void judulSalahDitolak() {
DaftarTugas daftar = new DaftarTugas();
assertThrows(MasukanInvalidException.class, () -> daftar.tambah(" "));
assertThrows(MasukanInvalidException.class,
() -> daftar.tambah("Beli|Pulpen"));
}
@Test
void simpanLaluMuatMempertahankanData() throws Exception {
Path berkas = Path.of("uji-junit.txt");
DaftarTugas daftar = new DaftarTugas();
daftar.tambah("Belajar Java dasar");
daftar.tambah("Baca dokumentasi Files");
daftar.tandaiSelesai("2");
PenyimpananTugas penyimpanan = new PenyimpananTugas(berkas);
penyimpanan.simpan(daftar);
DaftarTugas hasil = penyimpanan.muat();
assertEquals(2, hasil.semua().size());
assertTrue(hasil.semua().get(1).selesai());
}
}Padanannya: periksa(...) menjadi assertTrue/assertEquals, helper lempar menjadi assertThrows, dan ringkasan exit code menjadi laporan test runner. Saat build tool beserta repository dependency tersedia, pindahkan pemeriksaan satu kelompok per satu kelompok ke bentuk ini.
Berkas Final Aplikasi #
Tujuh berkas sumber membentuk proyek lengkap. Keenam berkas berikut tidak berubah sejak Bagian 3 — ditampilkan ulang agar halaman ini memuat proyek final secara utuh:
/**
* Satu baris tugas pada aplikasi catatan tugas.
* Class ini hanya menyimpan data; tidak membaca input dan tidak mencetak apa pun.
*/
public class Tugas {
private final int id;
private final String judul;
private boolean selesai;
/** Membuat tugas baru yang belum selesai. */
public Tugas(int id, String judul) {
this(id, judul, false);
}
/** Membuat tugas lengkap; dipakai saat memuat kembali dari berkas. */
public Tugas(int id, String judul, boolean selesai) {
this.id = id;
this.judul = judul;
this.selesai = selesai;
}
public int id() {
return id;
}
public String judul() {
return judul;
}
public boolean selesai() {
return selesai;
}
public void tandaiSelesai() {
this.selesai = true;
}
/** Bentuk baris siap tampil, misalnya "[ ] 1. Belajar Java dasar". */
public String formatBaris() {
String penanda = selesai ? "[x]" : "[ ]";
return penanda + " " + id + ". " + judul;
}
}/**
* Exception terperiksa untuk masukan pengguna yang tidak lolos validasi.
* Contoh: judul kosong atau id yang bukan angka.
*/
public class MasukanInvalidException extends Exception {
public MasukanInvalidException(String pesan) {
super(pesan);
}
}/**
* Exception terperiksa untuk id tugas yang tidak ada di dalam daftar.
*/
public class TugasTidakDitemukanException extends Exception {
public TugasTidakDitemukanException(int id) {
super("Tugas dengan id " + id + " tidak ditemukan.");
}
}import java.util.ArrayList;
import java.util.List;
/**
* Kumpulan tugas beserta seluruh aturan validasinya.
* Class ini menjadi satu-satunya pihak yang boleh mengubah isi daftar.
*/
public class DaftarTugas {
private final List<Tugas> daftar = new ArrayList<>();
/**
* Menambah tugas baru setelah memvalidasi judul.
* Id ditetapkan otomatis mengikuti id tertinggi yang sudah ada.
*/
public Tugas tambah(String judul) throws MasukanInvalidException {
if (judul == null) {
throw new MasukanInvalidException("Judul tidak boleh kosong.");
}
String judulBersih = judul.strip();
if (judulBersih.isEmpty()) {
throw new MasukanInvalidException("Judul tidak boleh kosong.");
}
if (judulBersih.length() > 100) {
throw new MasukanInvalidException(
"Judul " + judulBersih.length() + " karakter; maksimal 100.");
}
if (judulBersih.contains("|")) {
throw new MasukanInvalidException(
"Judul tidak boleh memuat karakter \"|\" karena dipakai pemisah berkas.");
}
Tugas tugas = new Tugas(idBerikutnya(), judulBersih);
daftar.add(tugas);
return tugas;
}
/**
* Menandai tugas dengan id pada teksId sebagai selesai.
* Masukan " 1 " tetap diterima karena spasi dipangkas lebih dulu.
*/
public Tugas tandaiSelesai(String teksId)
throws MasukanInvalidException, TugasTidakDitemukanException {
Tugas tugas = cari(teksId);
tugas.tandaiSelesai();
return tugas;
}
/** Salinan daftar saat ini; urutan mengikuti urutan penambahan. */
public List<Tugas> semua() {
return List.copyOf(daftar);
}
public boolean kosong() {
return daftar.isEmpty();
}
/**
* Memasang tugas hasil pembacaan berkas tanpa validasi ulang.
* Id dan status dibaca dari berkas apa adanya.
*/
void tambahMuatan(Tugas tugas) {
daftar.add(tugas);
}
private Tugas cari(String teksId)
throws MasukanInvalidException, TugasTidakDitemukanException {
int id;
try {
id = Integer.parseInt(teksId.strip());
} catch (NumberFormatException e) {
throw new MasukanInvalidException(
"Id harus berupa angka, diterima: \"" + teksId.strip() + "\".");
}
for (Tugas tugas : daftar) {
if (tugas.id() == id) {
return tugas;
}
}
throw new TugasTidakDitemukanException(id);
}
/** Id berikutnya mengikuti id tertinggi, bukan jumlah baris,
* agar tugas tetap unik bila berkas pernah diubah tangan. */
private int idBerikutnya() {
int tertinggi = 0;
for (Tugas tugas : daftar) {
tertinggi = Math.max(tertinggi, tugas.id());
}
return tertinggi + 1;
}
}import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* Menyimpan dan memuat DaftarTugas lewat berkas teks.
* Format satu baris: id|judul|status dengan status "aktif" atau "selesai".
*/
public class PenyimpananTugas {
private final Path berkas;
public PenyimpananTugas(Path berkas) {
this.berkas = berkas;
}
/** Menulis seluruh isi daftar ke berkas; berkas lama ditimpa penuh. */
public void simpan(DaftarTugas daftar) throws IOException {
List<String> baris = new ArrayList<>();
for (Tugas tugas : daftar.semua()) {
String status = tugas.selesai() ? "selesai" : "aktif";
baris.add(tugas.id() + "|" + tugas.judul() + "|" + status);
}
Files.write(berkas, baris, StandardCharsets.UTF_8);
}
/**
* Membaca berkas menjadi DaftarTugas.
* Baris kosong diabaikan; baris tanpa tiga bagian memicu IOException.
*/
public DaftarTugas muat() throws IOException {
DaftarTugas daftar = new DaftarTugas();
for (String baris : Files.readAllLines(berkas, StandardCharsets.UTF_8)) {
if (baris.isBlank()) {
continue;
}
String[] bagian = baris.split("\\|", 3);
if (bagian.length != 3) {
throw new IOException("Baris rusak: \"" + baris + "\"");
}
int id = Integer.parseInt(bagian[0].strip());
boolean selesai = bagian[2].strip().equals("selesai");
daftar.tambahMuatan(new Tugas(id, bagian[1], selesai));
}
return daftar;
}
}Sesi final aplikasi berikut menambahkan dua tugas, menandai tugas kedua selesai, lalu menampilkan daftar:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Scanner;
/**
* Titik masuk aplikasi catatan tugas versi ketiga:
* daftar dimuat dari berkas saat mulai dan disimpan setiap kali berubah.
*/
public class AplikasiTugas {
private static final Path BERKAS_DATA = Path.of("tugas.txt");
public static void main(String[] args) {
PenyimpananTugas penyimpanan = new PenyimpananTugas(BERKAS_DATA);
DaftarTugas daftar = new DaftarTugas();
if (Files.exists(BERKAS_DATA)) {
try {
daftar = penyimpanan.muat();
System.out.println("Memuat " + daftar.semua().size()
+ " tugas dari " + BERKAS_DATA + ".");
} catch (IOException e) {
System.out.println("Gagal memuat " + BERKAS_DATA + ": "
+ e.getMessage());
System.out.println("Meneruskan dengan daftar kosong.");
}
} else {
System.out.println("Belum ada berkas " + BERKAS_DATA
+ "; mulai dari daftar kosong.");
}
Scanner masukan = new Scanner(System.in);
boolean berjalan = true;
System.out.println("=== Aplikasi Catatan Tugas ===");
while (berjalan) {
tampilkanMenu();
String pilihan = masukan.nextLine().strip();
try {
switch (pilihan) {
case "1" -> {
tambahTugas(daftar, masukan);
simpan(penyimpanan, daftar);
}
case "2" -> tampilkanDaftar(daftar);
case "3" -> {
tandaiSelesai(daftar, masukan);
simpan(penyimpanan, daftar);
}
case "0" -> {
berjalan = false;
System.out.println("Sampai jumpa!");
}
default -> System.out.println(
"Pilihan tidak dikenal: " + pilihan);
}
} catch (MasukanInvalidException | TugasTidakDitemukanException e) {
System.out.println("Masukan ditolak: " + e.getMessage());
}
}
}
private static void tambahTugas(DaftarTugas daftar, Scanner masukan)
throws MasukanInvalidException {
System.out.print("Judul tugas: ");
Tugas tugas = daftar.tambah(masukan.nextLine());
System.out.println("Tugas ditambahkan: " + tugas.formatBaris());
}
private static void tandaiSelesai(DaftarTugas daftar, Scanner masukan)
throws MasukanInvalidException, TugasTidakDitemukanException {
System.out.print("Id tugas: ");
Tugas tugas = daftar.tandaiSelesai(masukan.nextLine());
System.out.println("Tugas ditandai selesai: " + tugas.formatBaris());
}
private static void simpan(PenyimpananTugas penyimpanan, DaftarTugas daftar) {
try {
penyimpanan.simpan(daftar);
System.out.println("Daftar disimpan ke tugas.txt.");
} catch (IOException e) {
System.out.println("Gagal menyimpan tugas.txt: " + e.getMessage());
}
}
private static void tampilkanMenu() {
System.out.println();
System.out.println("Menu:");
System.out.println("1. Tambah tugas");
System.out.println("2. Lihat daftar tugas");
System.out.println("3. Tandai tugas selesai");
System.out.println("0. Keluar");
System.out.print("Pilihan: ");
}
private static void tampilkanDaftar(DaftarTugas daftar) {
System.out.println();
List<Tugas> semua = daftar.semua();
if (semua.isEmpty()) {
System.out.println("Belum ada tugas.");
return;
}
System.out.println("Daftar tugas:");
for (Tugas tugas : semua) {
System.out.println(tugas.formatBaris());
}
}
}Output yang Dihasilkan:
Belum ada berkas tugas.txt; mulai dari daftar kosong.
=== Aplikasi Catatan Tugas ===
Menu:
1. Tambah tugas
2. Lihat daftar tugas
3. Tandai tugas selesai
0. Keluar
Pilihan: Judul tugas: Tugas ditambahkan: [ ] 1. Membuat laporan bulanan
Daftar disimpan ke tugas.txt.
Menu:
1. Tambah tugas
2. Lihat daftar tugas
3. Tandai tugas selesai
0. Keluar
Pilihan: Judul tugas: Tugas ditambahkan: [ ] 2. Mengarsipkan dokumen
Daftar disimpan ke tugas.txt.
Menu:
1. Tambah tugas
2. Lihat daftar tugas
3. Tandai tugas selesai
0. Keluar
Pilihan: Id tugas: Tugas ditandai selesai: [x] 2. Mengarsipkan dokumen
Daftar disimpan ke tugas.txt.
Menu:
1. Tambah tugas
2. Lihat daftar tugas
3. Tandai tugas selesai
0. Keluar
Pilihan:
Daftar tugas:
[ ] 1. Membuat laporan bulanan
[x] 2. Mengarsipkan dokumen
Menu:
1. Tambah tugas
2. Lihat daftar tugas
3. Tandai tugas selesai
0. Keluar
Pilihan: Sampai jumpa!Kompilasi dan Perakitan JAR #
Tahap distribusi mengikuti pola chapter Packaging Aplikasi CLI: kompilasi seluruh berkas sumber, bungkus bytecode menjadi satu arsip JAR dengan titik masuk di manifest, lalu jalankan arsip tersebut:
Sesi berikut dikutip dari eksekusi sungguhan dengan JDK 25:
$ javac -d kelas *.java
$ jar --create --file tugas.jar --main-class AplikasiTugas -C kelas .
$ jar --list --file tugas.jar
META-INF/
META-INF/MANIFEST.MF
AplikasiTugas.class
DaftarTugas.class
MasukanInvalidException.class
PenyimpananTugas.class
Tugas.class
TugasTidakDitemukanException.class
UjiDaftarTugas$Perintah.class
UjiDaftarTugas.class
$ printf '1\nKirim laporan mingguan\n0\n' | java -jar tugas.jar
Belum ada berkas tugas.txt; mulai dari daftar kosong.
=== Aplikasi Catatan Tugas ===
Menu:
1. Tambah tugas
2. Lihat daftar tugas
3. Tandai tugas selesai
0. Keluar
Pilihan: Judul tugas: Tugas ditambahkan: [ ] 1. Kirim laporan mingguan
Daftar disimpan ke tugas.txt.
Menu:
1. Tambah tugas
2. Lihat daftar tugas
3. Tandai tugas selesai
0. Keluar
Pilihan: Sampai jumpa!Mari kita telaah perintah-perintahnya:
- Baris
javac -d kelas *.java: compiler menerjemahkan ketujuh berkas sumber sekaligus dan menaruh hasilnya dikelas/. Hasilnya delapan berkas.class— antarmuka bersarangPerintahmenjadi berkas sendiri dengan tanda$. - Baris
jar --create --file tugas.jar --main-class AplikasiTugas -C kelas .: alatjarmembungkus seluruh isikelas/menjadi arsip, dan--main-class AplikasiTugasmenuliskan atributMain-Classke manifest di dalam arsip. - Hasil
jar --list: isi arsip hanyalahMETA-INF/,MANIFEST.MF, dan delapan berkas.class— JAR memang arsip zip dengan struktur tambahan yang sederhana. - Manifest hasilnya tiga baris:
Manifest-Version: 1.0,Created-By: 25.0.4.1 (Eclipse Adoptium), danMain-Class: AplikasiTugas— baris terakhir dibaca peluncurjava -jar. - Baris
printf ... | java -jar tugas.jar: peluncur membaca manifest, menjalankanAplikasiTugas.main, meneruskan pipa stdin sebagai masukan, dan menulistugas.txtdi direktori kerja.
Harness pengujian ikut berada di dalam arsip dan dapat dijalankan tanpa membongkar JAR:
$ java -cp tugas.jar UjiDaftarTugas
PASS tambah mengembalikan id pertama 1
PASS tambah memangkas spasi judul
PASS tugas baru belum selesai
PASS tugas kedua mendapat id 2
PASS judul kosong ditolak
PASS judul 101 karakter ditolak
PASS judul memuat karakter | ditolak
PASS id bukan angka ditolak
PASS id tidak ada ditolak
PASS tugas bisa ditandai selesai
PASS format baris selesai benar
PASS berkas tersimpan
PASS baris pertama berkas benar
PASS jumlah tugas termuat kembali
PASS judul termuat kembali
PASS status selesai termuat kembali
PASS baris rusak memicu IOException
Pengujian selesai; jumlah gagal: 0.
$ cat tugas.txt
1|Kirim laporan mingguan|aktifPerintah java -cp tugas.jar UjiDaftarTugas memakai mode classpath: lokasi kelas dinyatakan lewat -cp dan nama class eksplisit, sehingga manifest tidak dibaca — cocok untuk program sekunder di dalam arsip yang sama. Exit code 0 menandakan seluruh pemeriksaan lulus dari dalam JAR.
Proyek aplikasi command line tuntas: kerangka dan input di Bagian 1, validasi dan exception di Bagian 2, penyimpanan berkas di Bagian 3, lalu pengujian dan JAR di Bagian 4. Proyek berikutnya, Sistem Manajemen Perpustakaan, menaikkan bobot pemodelan: domain buku, anggota, dan transaksi peminjaman yang diatur lewat class-class yang saling bekerja sama.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiTugas.java
Unduh berkasimport java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Scanner;
/**
* Titik masuk aplikasi catatan tugas versi ketiga:
* daftar dimuat dari berkas saat mulai dan disimpan setiap kali berubah.
*/
public class AplikasiTugas {
private static final Path BERKAS_DATA = Path.of("tugas.txt");
public static void main(String[] args) {
PenyimpananTugas penyimpanan = new PenyimpananTugas(BERKAS_DATA);
DaftarTugas daftar = new DaftarTugas();
if (Files.exists(BERKAS_DATA)) {
try {
daftar = penyimpanan.muat();
System.out.println("Memuat " + daftar.semua().size()
+ " tugas dari " + BERKAS_DATA + ".");
} catch (IOException e) {
System.out.println("Gagal memuat " + BERKAS_DATA + ": "
+ e.getMessage());
System.out.println("Meneruskan dengan daftar kosong.");
}
} else {
System.out.println("Belum ada berkas " + BERKAS_DATA
+ "; mulai dari daftar kosong.");
}
Scanner masukan = new Scanner(System.in);
boolean berjalan = true;
System.out.println("=== Aplikasi Catatan Tugas ===");
while (berjalan) {
tampilkanMenu();
String pilihan = masukan.nextLine().strip();
try {
switch (pilihan) {
case "1" -> {
tambahTugas(daftar, masukan);
simpan(penyimpanan, daftar);
}
case "2" -> tampilkanDaftar(daftar);
case "3" -> {
tandaiSelesai(daftar, masukan);
simpan(penyimpanan, daftar);
}
case "0" -> {
berjalan = false;
System.out.println("Sampai jumpa!");
}
default -> System.out.println(
"Pilihan tidak dikenal: " + pilihan);
}
} catch (MasukanInvalidException | TugasTidakDitemukanException e) {
System.out.println("Masukan ditolak: " + e.getMessage());
}
}
}
private static void tambahTugas(DaftarTugas daftar, Scanner masukan)
throws MasukanInvalidException {
System.out.print("Judul tugas: ");
Tugas tugas = daftar.tambah(masukan.nextLine());
System.out.println("Tugas ditambahkan: " + tugas.formatBaris());
}
private static void tandaiSelesai(DaftarTugas daftar, Scanner masukan)
throws MasukanInvalidException, TugasTidakDitemukanException {
System.out.print("Id tugas: ");
Tugas tugas = daftar.tandaiSelesai(masukan.nextLine());
System.out.println("Tugas ditandai selesai: " + tugas.formatBaris());
}
private static void simpan(PenyimpananTugas penyimpanan, DaftarTugas daftar) {
try {
penyimpanan.simpan(daftar);
System.out.println("Daftar disimpan ke tugas.txt.");
} catch (IOException e) {
System.out.println("Gagal menyimpan tugas.txt: " + e.getMessage());
}
}
private static void tampilkanMenu() {
System.out.println();
System.out.println("Menu:");
System.out.println("1. Tambah tugas");
System.out.println("2. Lihat daftar tugas");
System.out.println("3. Tandai tugas selesai");
System.out.println("0. Keluar");
System.out.print("Pilihan: ");
}
private static void tampilkanDaftar(DaftarTugas daftar) {
System.out.println();
List<Tugas> semua = daftar.semua();
if (semua.isEmpty()) {
System.out.println("Belum ada tugas.");
return;
}
System.out.println("Daftar tugas:");
for (Tugas tugas : semua) {
System.out.println(tugas.formatBaris());
}
}
}
DaftarTugas.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
/**
* Kumpulan tugas beserta seluruh aturan validasinya.
* Class ini menjadi satu-satunya pihak yang boleh mengubah isi daftar.
*/
public class DaftarTugas {
private final List<Tugas> daftar = new ArrayList<>();
/**
* Menambah tugas baru setelah memvalidasi judul.
* Id ditetapkan otomatis mengikuti id tertinggi yang sudah ada.
*/
public Tugas tambah(String judul) throws MasukanInvalidException {
if (judul == null) {
throw new MasukanInvalidException("Judul tidak boleh kosong.");
}
String judulBersih = judul.strip();
if (judulBersih.isEmpty()) {
throw new MasukanInvalidException("Judul tidak boleh kosong.");
}
if (judulBersih.length() > 100) {
throw new MasukanInvalidException(
"Judul " + judulBersih.length() + " karakter; maksimal 100.");
}
if (judulBersih.contains("|")) {
throw new MasukanInvalidException(
"Judul tidak boleh memuat karakter \"|\" karena dipakai pemisah berkas.");
}
Tugas tugas = new Tugas(idBerikutnya(), judulBersih);
daftar.add(tugas);
return tugas;
}
/**
* Menandai tugas dengan id pada teksId sebagai selesai.
* Masukan " 1 " tetap diterima karena spasi dipangkas lebih dulu.
*/
public Tugas tandaiSelesai(String teksId)
throws MasukanInvalidException, TugasTidakDitemukanException {
Tugas tugas = cari(teksId);
tugas.tandaiSelesai();
return tugas;
}
/** Salinan daftar saat ini; urutan mengikuti urutan penambahan. */
public List<Tugas> semua() {
return List.copyOf(daftar);
}
public boolean kosong() {
return daftar.isEmpty();
}
/**
* Memasang tugas hasil pembacaan berkas tanpa validasi ulang.
* Id dan status dibaca dari berkas apa adanya.
*/
void tambahMuatan(Tugas tugas) {
daftar.add(tugas);
}
private Tugas cari(String teksId)
throws MasukanInvalidException, TugasTidakDitemukanException {
int id;
try {
id = Integer.parseInt(teksId.strip());
} catch (NumberFormatException e) {
throw new MasukanInvalidException(
"Id harus berupa angka, diterima: \"" + teksId.strip() + "\".");
}
for (Tugas tugas : daftar) {
if (tugas.id() == id) {
return tugas;
}
}
throw new TugasTidakDitemukanException(id);
}
/** Id berikutnya mengikuti id tertinggi, bukan jumlah baris,
* agar tugas tetap unik bila berkas pernah diubah tangan. */
private int idBerikutnya() {
int tertinggi = 0;
for (Tugas tugas : daftar) {
tertinggi = Math.max(tertinggi, tugas.id());
}
return tertinggi + 1;
}
}
MasukanInvalidException.java
Unduh berkas/**
* Exception terperiksa untuk masukan pengguna yang tidak lolos validasi.
* Contoh: judul kosong atau id yang bukan angka.
*/
public class MasukanInvalidException extends Exception {
public MasukanInvalidException(String pesan) {
super(pesan);
}
}
PenyimpananTugas.java
Unduh berkasimport java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* Menyimpan dan memuat DaftarTugas lewat berkas teks.
* Format satu baris: id|judul|status dengan status "aktif" atau "selesai".
*/
public class PenyimpananTugas {
private final Path berkas;
public PenyimpananTugas(Path berkas) {
this.berkas = berkas;
}
/** Menulis seluruh isi daftar ke berkas; berkas lama ditimpa penuh. */
public void simpan(DaftarTugas daftar) throws IOException {
List<String> baris = new ArrayList<>();
for (Tugas tugas : daftar.semua()) {
String status = tugas.selesai() ? "selesai" : "aktif";
baris.add(tugas.id() + "|" + tugas.judul() + "|" + status);
}
Files.write(berkas, baris, StandardCharsets.UTF_8);
}
/**
* Membaca berkas menjadi DaftarTugas.
* Baris kosong diabaikan; baris tanpa tiga bagian memicu IOException.
*/
public DaftarTugas muat() throws IOException {
DaftarTugas daftar = new DaftarTugas();
for (String baris : Files.readAllLines(berkas, StandardCharsets.UTF_8)) {
if (baris.isBlank()) {
continue;
}
String[] bagian = baris.split("\\|", 3);
if (bagian.length != 3) {
throw new IOException("Baris rusak: \"" + baris + "\"");
}
int id = Integer.parseInt(bagian[0].strip());
boolean selesai = bagian[2].strip().equals("selesai");
daftar.tambahMuatan(new Tugas(id, bagian[1], selesai));
}
return daftar;
}
}
Tugas.java
Unduh berkas/**
* Satu baris tugas pada aplikasi catatan tugas.
* Class ini hanya menyimpan data; tidak membaca input dan tidak mencetak apa pun.
*/
public class Tugas {
private final int id;
private final String judul;
private boolean selesai;
/** Membuat tugas baru yang belum selesai. */
public Tugas(int id, String judul) {
this(id, judul, false);
}
/** Membuat tugas lengkap; dipakai saat memuat kembali dari berkas. */
public Tugas(int id, String judul, boolean selesai) {
this.id = id;
this.judul = judul;
this.selesai = selesai;
}
public int id() {
return id;
}
public String judul() {
return judul;
}
public boolean selesai() {
return selesai;
}
public void tandaiSelesai() {
this.selesai = true;
}
/** Bentuk baris siap tampil, misalnya "[ ] 1. Belajar Java dasar". */
public String formatBaris() {
String penanda = selesai ? "[x]" : "[ ]";
return penanda + " " + id + ". " + judul;
}
}
TugasTidakDitemukanException.java
Unduh berkas/**
* Exception terperiksa untuk id tugas yang tidak ada di dalam daftar.
*/
public class TugasTidakDitemukanException extends Exception {
public TugasTidakDitemukanException(int id) {
super("Tugas dengan id " + id + " tidak ditemukan.");
}
}
UjiDaftarTugas.java
Unduh berkasimport java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Pengujian perilaku inti aplikasi tanpa pustaka eksternal.
* Setiap pemeriksaan mencetak PASS atau FAIL; exit code 0 berarti semua lulus.
*/
public class UjiDaftarTugas {
private static int gagal = 0;
public static void main(String[] args) throws Exception {
ujiTambah();
ujiValidasiJudul();
ujiTandaiSelesai();
ujiSimpanMuat();
ujiBerkasRusak();
System.out.println();
System.out.println("Pengujian selesai; jumlah gagal: " + gagal + ".");
if (gagal > 0) {
System.exit(1);
}
}
private static void ujiTambah() throws MasukanInvalidException {
DaftarTugas daftar = new DaftarTugas();
Tugas tugas = daftar.tambah(" Belajar Java dasar ");
periksa("tambah mengembalikan id pertama 1", tugas.id() == 1);
periksa("tambah memangkas spasi judul",
tugas.judul().equals("Belajar Java dasar"));
periksa("tugas baru belum selesai", !tugas.selesai());
Tugas kedua = daftar.tambah("Baca dokumentasi Files");
periksa("tugas kedua mendapat id 2", kedua.id() == 2);
}
private static void ujiValidasiJudul() {
periksa("judul kosong ditolak", ditolak(" "));
periksa("judul 101 karakter ditolak", ditolak("x".repeat(101)));
periksa("judul memuat karakter | ditolak", ditolak("Beli|Pulpen"));
}
private static void ujiTandaiSelesai()
throws MasukanInvalidException, TugasTidakDitemukanException {
DaftarTugas daftar = new DaftarTugas();
daftar.tambah("Belajar Java dasar");
periksa("id bukan angka ditolak",
lempar(() -> daftar.tandaiSelesai("abc"),
MasukanInvalidException.class));
periksa("id tidak ada ditolak",
lempar(() -> daftar.tandaiSelesai("9"),
TugasTidakDitemukanException.class));
Tugas tugas = daftar.tandaiSelesai("1");
periksa("tugas bisa ditandai selesai", tugas.selesai());
periksa("format baris selesai benar",
tugas.formatBaris().equals("[x] 1. Belajar Java dasar"));
}
private static void ujiSimpanMuat() throws Exception {
Path berkas = Path.of("uji-tugas.txt");
DaftarTugas daftar = new DaftarTugas();
daftar.tambah("Belajar Java dasar");
daftar.tambah("Baca dokumentasi Files");
daftar.tandaiSelesai("2");
PenyimpananTugas penyimpanan = new PenyimpananTugas(berkas);
penyimpanan.simpan(daftar);
periksa("berkas tersimpan", Files.exists(berkas));
periksa("baris pertama berkas benar",
Files.readAllLines(berkas, StandardCharsets.UTF_8).get(0)
.equals("1|Belajar Java dasar|aktif"));
DaftarTugas hasil = penyimpanan.muat();
periksa("jumlah tugas termuat kembali",
hasil.semua().size() == 2);
periksa("judul termuat kembali",
hasil.semua().get(0).judul().equals("Belajar Java dasar"));
periksa("status selesai termuat kembali",
hasil.semua().get(1).selesai());
}
private static void ujiBerkasRusak() throws IOException {
Path berkas = Path.of("uji-rusak.txt");
Files.writeString(berkas, "baris tanpa pemisah\n", StandardCharsets.UTF_8);
PenyimpananTugas penyimpanan = new PenyimpananTugas(berkas);
periksa("baris rusak memicu IOException",
lempar(penyimpanan::muat, IOException.class));
}
/** Mencetak hasil satu pemeriksaan dan menghitung kegagalan. */
private static void periksa(String nama, boolean kondisi) {
if (kondisi) {
System.out.println("PASS " + nama);
} else {
System.out.println("FAIL " + nama);
gagal++;
}
}
/** Menjalankan tambah; true bila judul ditolak oleh validasi. */
private static boolean ditolak(String judul) {
try {
new DaftarTugas().tambah(judul);
return false;
} catch (MasukanInvalidException e) {
return true;
}
}
/** Menjalankan perintah; true bila melempar exception bertipe persis. */
private static boolean lempar(Perintah perintah,
Class<? extends Exception> jenis) {
try {
perintah.jalankan();
return false;
} catch (Exception e) {
return e.getClass() == jenis;
}
}
/** Antarmuka satu method agar blok uji bisa melempar exception terperiksa. */
interface Perintah {
void jalankan() throws Exception;
}
}