diff --git a/README.md b/README.md index 4cce06a..57f0bec 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ Denne mappen inneholder øvingstekster for TDT4100 - Objektorientert programmeri | -------------------------------------------- | ----------------------------------------- | | [Øving 0](./oppgavetekster/oving0/README.md) | Introduksjon og oppsett av Java | | [Øving 1](./oppgavetekster/oving1/README.md) | Java-syntaks og objektorientert tankegang | +| [Øving 2](./oppgavetekster/oving2/README.md) | Innkapsling og validering | diff --git a/oppgavetekster/oving2/Account.md b/oppgavetekster/oving2/Account.md new file mode 100644 index 0000000..c5e15a0 --- /dev/null +++ b/oppgavetekster/oving2/Account.md @@ -0,0 +1,38 @@ +# Innkapsling - Account-oppgave + +Oppgaven er en innkapslet og litt utvidet variant av [Account-oppgaven](../oving1/Account.md) under temaet [Tilstand og oppførsel](https://www.ntnu.no/wiki/pages/viewpage.action?pageId=65937373), og stiller blant annet større krav til validering. + +Dersom du ikke har gjort [Account-oppgaven](../oving1/Account.md), bør du gjøre den først. Løsningsforslag til denne oppgaven kommer til å bli tilgjengelig [her](https://git.ntnu.no/tdt4100/tdt4100-lf-25/blob/main/src/main/java/oving1/Account.java) etter siste demonstrasjonsfrist for øving 1. + +Et `Account`-objekt inneholder data om beløpet som står på kontoen og rentefoten (prosentpoeng). + +Begge verdiene skal oppgis og settes når objektet opprettes og ingen av verdiene kan være negative. + +`Account`-klassen har metoder for å sette inn og ta ut beløp, og legge til påløpte renter, i tillegg til en konstruktør for å initialisere en ny konto. Alle disse skal utløse unntak av typen `IllegalArgumentException`, dersom et argument ikke tilfredstiller kravene som angis. + +- `Account(double, double)` - konstruktøren skal ta inn startbeløpet og rentefoten (prosentpoeng). Ingen av disse kan være negative. +- `double getBalance()` - returnerer beløpet som står på kontoen. +- `double getInterestRate()` - returnerer renten på kontoen. +- `void setInterestRate(double)` - denne metoden tar inn en ikke-negativ verdi og setter renten til denne verdien. +- `void deposit(double)` - denne metoden tar inn et ikke-negativt beløp og øker konto-beløpet tilsvarende. +- `void withdraw(double)` - denne metoden tar inn et ikke-negativt beløp og minsker konto-beløpet tilsvarende. Dersom det nye konto-beløpet er negativt, så skal tilstanden ikke endre, og det skal utløses et unntak av typen `IllegalArgumentException`. +- `void addInterest()` - beregner renta og legger det til konto-beløpet. + +## Leseliste + +- [Gyldig tilstand](https://www.ntnu.no/wiki/display/tdt4100/Gyldig+tilstand) - Tilstanden til et objekt er verdien av alle attributtene. En viktig del av [oppførselen til et objekt](https://www.ntnu.no/wiki/pages/viewpage.action?pageId=65937373) er å sikre at tilstanden til objektet alltid er _gyldig_, dvs. at alle attributtene har gyldige/konsistente verdier. +- [Innkapsling](https://www.ntnu.no/wiki/display/tdt4100/Innkapsling) - Innkapsling er en programmeringsteknikk som har som formål å hindre direkte tilgang til tilstanden til et objekt fra objekter av andre klasser. +- [Koding av valideringsmetoder](https://www.ntnu.no/wiki/display/tdt4100/Koding+av+valideringsmetoder) - En valideringsmetode har som formål å sjekke om en eller flere verdier er gyldige, slik at dette kan sjekkes av f.eks. setter-metoder før tilsvarende attributter evt. settes. + +## Del 1: Innkapsling og validering - Teori + +Ta utgangspunkt i koden fra [Account](../oving1/Account.md)-klassen og besvar følgende spørsmål: + +- Forklar hvorfor metodene over kan sies å være en komplett innkapsling av tilstanden? +- Er denne klassen data-orientert eller tjeneste-orientert? Begrunn svaret! + +## Del 2 - Java-kode + +Implementer endringene fra [Account](../oving1/Account.md)-klassen i den nye `Account`-klassen med oppførsel som beskrevet over. + +Testkode for denne oppgaven finner du i [oving2/AccountTest.java](../../src/test/java/oving2/AccountTest.java). diff --git a/oppgavetekster/oving2/Encapsulation.md b/oppgavetekster/oving2/Encapsulation.md new file mode 100644 index 0000000..d5429c6 --- /dev/null +++ b/oppgavetekster/oving2/Encapsulation.md @@ -0,0 +1,38 @@ +# Innkapsling og gyldig tilstand - Oppgave om innkapsling og validering av klasser + +I denne oppgaven skal du velge tre oppgaver som du har gjort i øving 1 fra listen nedenfor, og innkapsle og validere disse klassene. Dersom du ikke har gjort tre oppgaver, bør du gjøre dette først. Løsningsforslag til øving 1 kommer til å befinne seg [her](https://git.ntnu.no/tdt4100/tdt4100-lf-25/blob/main/src/main/java/oving1) etter siste demonstrasjonsfrist for øving 1. + +Skriv svar (stikkord/få, korte setninger) på spørsmål 1-4 (fra del 1 nedenfor) som kommentarer i koden din. + +__Oppgaver__: + +- [Digit-oppgave](../oving1/Digit.md) (Lett) +- [UpOrDownCounter-oppgave](../oving1/UpOrDownCounter.md) (Lett) +- [Location-oppgave](../oving1/Location.md) (Lett) +- [StopWatch-oppgave](../oving1/StopWatch.md) (Medium) +- [LineEditor-oppgave med fri peker](./LineEditor.md) (Vanskelig) + +Merk at spesifikasjonen for [LineEditor](../oving1/LineEditor.md) er litt utvidet for denne oppgaven. Se [LineEditor-oppgave med fri peker](./LineEditor.md). + +## Del 1: Innkapsling og validering - Teori + +Ta utgangspunkt i (koden for) den originale klassen og besvar følgende spørsmål: + +- Hvordan skal private og public brukes for at denne klassen skal være korrekt innkapslet? +- Hva slags validering bør legges til for å sikre gyldig tilstand? +- Hvilke metoder må evt. legges til? +- Vil du karakterisere denne klassen som data-orientert eller tjeneste-orientert. Begrunn svaret! + +## Del 2: Java-kode + +Implementer endringene foreslått i punktene 1-3 og prøv ut klassene. Husk å kopiere koden din fra mappen i øving 1 til [`src/main/java/oving2`](../../src/main/java/oving2)! + +Testkoder for denne oppgaven finner du her: + +- [oving2/DigitTest.java](../../src/test/java/oving2/DigitTest.java). +- [oving2/UpOrDownCounterTest.java](../../src/test/java/oving2/UpOrDownCounterTest.java). +- [oving2/LocationTest.java](../../src/test/java/oving2/LocationTest.java). +- [oving2/StopWatchTest.java](../../src/test/java/oving2/StopWatchTest.java). +- [oving2/LineEditorTest.java](../../src/test/java/oving2/LineEditorTest.java). + +Testkodene viser om du har innkapslet på samme måte som fagstaben har gjort. Din kode kan fungere selv om testene feiler, dersom du har valgt en løsere/strammere innkapsling iht. argumentasjonen i 1-3. Er du enig med hvordan fagstaben har gjort det? diff --git a/oppgavetekster/oving2/LineEditor.md b/oppgavetekster/oving2/LineEditor.md new file mode 100644 index 0000000..9ecdd63 --- /dev/null +++ b/oppgavetekster/oving2/LineEditor.md @@ -0,0 +1,16 @@ +# Gyldig tilstand - LineEditor-oppgave med fri peker + +Oppgaven utvider [Tilstand og oppførsel - LineEditor-oppgave](../oving1/LineEditor.md) med validering. + +Denne oppgaven tar utgangspunkt i [Tilstand og oppførsel - LineEditor-oppgave](../oving1/LineEditor.md) og utvider `LineEditor`-klassen med metoder for å endre teksten og tekstinnsettingsposisjonen direkte, så det blir enklere å gjøre om tilstanden til objektet. + +Dersom du ikke har gjort [LineEditor-oppgaven](../oving1/LineEditor.md), bør du gjøre den først. Løsningsforslag til denne oppgaven kommer til å bli tilgjengelig [her](https://git.ntnu.no/tdt4100/tdt4100-lf-25/blob/main/src/main/java/oving1/LineEditor.java) etter siste demonstrasjonsfrist for øving 1. + +Endringer: + +- Når teksten endres skal tekstinnsettingsposisjonen settes til å være bak teksten. +- Det skal ikke være mulig å passere `null` som tekst. + +Hvordan vil du implementere dette med én eller flere metoder, inkludert valideringsmetode(r), slik at en er sikret at `LineEditor`-objekter aldri blir ugyldige? + +Testkode for denne oppgaven finner du her: [oving2/LineEditorTest.java](../../src/test/java/oving2/LineEditorTest.java). diff --git a/oppgavetekster/oving2/Person.md b/oppgavetekster/oving2/Person.md new file mode 100644 index 0000000..b12aabf --- /dev/null +++ b/oppgavetekster/oving2/Person.md @@ -0,0 +1,59 @@ +# Innkapsling - Person-oppgave + +Oppgaven handler om en `Person`-klasse, som håndterer informasjon om en person (navn, e-post, fødselsdato og kjønn) og implementerer innkapslingsmetoder med validering. + +Et `Person`-objekt inneholder _navn_ (både fornavn og etternavn), _e-post_, _fødselsdag_ og _kjønn_: + +- Navnet inneholder både fornavn og etternavn (og ingen mellomnavn), som begge må være på minst to bokstaver langt, navnene må være skilt med ett mellomrom og kun inneholde bokstaver. +- E-post-adressen (hvis den ikke er `null`) må være på formen `fornavn.etternavn@domene.landskode`, f.eks. `hallvard.trætteberg@ntnu.no` (en liste over landskoder finner du [her](http://pastebin.com/chG6WLWF)). +- Fødselsdagen skal være et dato-objekt (java.util.Date) og kan ikke være frem i tid. +- En persons kjønn skal kunne returneres som `'M'`, `'F'` eller `'\0'` (null-tegnet). + +`Person`-klassen har tilgangsmetoder for å hente og sette tilstandene. Dersom et argument er ugyldig i seg selv, så skal unntaket `IllegalArgumentException` utløses. + +- `void setName(String)` - oppdaterer navnet (fornavn og etternavn med mellomrom mellom), dersom det er gyldig i henhold til kravene over. Det er greit om navnet som settes, ikke stemmer med e-post-adressen. Kast et passende unntak dersom navnet er `null` eller dersom e-post-adressen allerede har blitt satt. +- `void setEmail(String)` - oppdaterer e-post-adressen, etter å ha sjekket at den stemmer med navnet. Kast et passende unntak dersom e-post-adressen er `null` eller dersom navnet enda ikke har blitt satt. +- `void setBirthday(Date)` - oppdaterer fødselsdatoen. Kast et passende unntak dersom fødselsdatoen er `null` eller ligger i fremtiden. +- `void setGender(char)` - oppdaterer kjønnet. Kast et passende unntak dersom kjønnet er ugyldig. + +I tillegg til disse såkalte _setter_-metodene, så må `Person`-klassen ha tilsvarende _getter_-metoder. + +## Leseliste + +- [Gyldig tilstand](https://www.ntnu.no/wiki/display/tdt4100/Gyldig+tilstand) - Tilstanden til et objekt er verdien av alle attributtene. En viktig del av [oppførselen til et objekt](https://www.ntnu.no/wiki/pages/viewpage.action?pageId=65937373) er å sikre at tilstanden til objektet alltid er _gyldig_, dvs. at alle attributtene har gyldige/konsistente verdier. +- [Innkapsling](https://www.ntnu.no/wiki/display/tdt4100/Innkapsling) - Innkapsling er en programmeringsteknikk som har som formål å hindre direkte tilgang til tilstanden til et objekt fra objekter av andre klasser. +- [Koding av valideringsmetoder](https://www.ntnu.no/wiki/display/tdt4100/Koding+av+valideringsmetoder) - En valideringsmetode har som formål å sjekke om en eller flere verdier er gyldige, slik at dette kan sjekkes av f.eks. setter-metoder før tilsvarende attributter evt. settes. +- [String-klassen](https://www.ntnu.no/wiki/display/tdt4100/java.lang.String) - Siden gir en innføring i String-klassen og en oversikt over nyttig String-metoder. + +## Del 1 – Java-kode + +Implementer `Person`-klassen med stram innkapsling. Eventuelle hjelpemetoder for validering bør også ha stram innkapsling. Det kan være lurt å lese om [String-klassen](https://www.ntnu.no/wiki/display/tdt4100/java.lang.String) og dens metoder før du setter i gang. + +Testkode for denne oppgaven finner du i [oving2/PersonTest.java](../../src/test/java/oving2/PersonTest.java). + +Merk at din implementasjon må ligge i en pakke med samme navn som testkodens pakke. Pass derfor på at Person-klassen ligger i pakken `oving2`. + +## Del 2 - Spørsmål om innkapsling + +- Foreslå en alternativ innkapsling av navnet. Hint: del opp. +- Foreslå _to_ alternative strategier for å kapsle inn tilstand som er koblet slik navn og e-post er. Hint: 1) samtidig og 2) dekoble. + +## Ekstraoppgave - Personnummer + +Utvid klassen med en persons personnummer. Personnummeret kan ikke settes før kjønn og fødselsdag er satt. + +Et personnummer består grovt sett av fødselsdatoen, et (vilkårlig) løpenummer og to kontrollsifre. Kontrollsifrene gjør det enklere å sjekke om et personnummer er ekte. Mer spesifikt er reglene for personnummer som følger: + +- Et personnummer består av 11 siffer, med følgende struktur: **D1D2**M1M2**Y1Y2**N1N2N3**K1K2** (fet skrift for lesbarhet). +- De seks første sifrene, **D1D2**M1M2**Y1Y2**, tilsvarer fødselsdatoens dag (1-31), måned (1-12) og år (0-99). +- De tre neste sifrene, N1N2N3, kan antas å være vilkårlige, men N3 må være partall for kvinner og oddetall for menn. +- De to siste sifrene, K1K2, er kontrollsifre, som hver for seg beregnes ut fra de foregående sifrene. Formelen for dem begge er `11 – (VS % 11)`, hvor VS (veid sum) for `K1` er `D1*F1 + D2*F2 + … + N2*F8 + N3*F9` og `VS` for `K2` er `D1*G1 + D2*G2 + … + N3*G9 + K1*G10`. F’ene og G’ene er oppgitt i tabellen under. Dersom formelen gir tallet 11 så skal verdien 0 brukes isteden. Om både K1 og K2 stemmer med kontrollsifferne generert basert på formlene over, så er kontrollsifferne i personnummeret gyldig. + +| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| F | 3 | 7 | 6 | 1 | 8 | 9 | 4 | 5 | 2 | | +| G | 5 | 4 | 3 | 2 | 7 | 6 | 5 | 4 | 3 | 2 | + +Implementer kode for å sette (med metoden `setSSN(String)`) og validere et gyldig personnummer. Du må også kaste passende unntak dersom brukeren prøver å endre tilstanden slik at personnummeret ville blitt ugyldig. + +Testkode for denne oppgaven finner du i [oving2/PersonTest2.java](../../src/test/java/oving2/PersonTest2.java). diff --git a/oppgavetekster/oving2/README.md b/oppgavetekster/oving2/README.md new file mode 100644 index 0000000..f0ce2b7 --- /dev/null +++ b/oppgavetekster/oving2/README.md @@ -0,0 +1,46 @@ +# Øving 2: Innkapsling og vali⁣dering + +**Øvingsmål**: + +- Lære å innkapsle klasser og metoder etter god programmeringsskikk +- Lære å validere argumenter for å sikre gyldig tilstand + +**Øvingskrav**: + +- Kunne forstå og implementere hvordan en klasse best bør innkapsles +- Kunne skrive kode for å validere argumenter for å sikre gyldig tilstand +- Kunne utløse exceptions ved ugyldige argumenter i en metode + +## Dette må du gjøre + +### Del 1: Teori + +Les [wikisiden om innkapsling](https://www.ntnu.no/wiki/display/tdt4100/Innkapsling) og svar på følgende: + +- Hva er en **synlighetsmodifikator**? +- Hva er forskjellen på **private** og **public** og når brukes de? + +Teori-oppgavene besvares i en tekstfil eller på papir, og gjennomgås med studass ved godkjenning. + +### Del 2: Programmering + +Velg minst 2 av følgende oppgaver: + +- [Innkapsling og validering av 3 eksisterende klasser](./Encapsulation.md) (Varierende) +- [Account](./Account.md) (Lett) +- [Person](./Person.md) (Medium) +- [Vehicle](./Vehicle.md) (Medium) + +Oppgavene for denne øvingen skal lagres i [`src/main/java/oving2`](../../src/main/java/oving2). Test-filene som kjøres for å teste koden ligger i [`src/test/java/oving2`](../../src/test/java/oving2). + +Oppgavene er markert med en vanskeliggrad relativt til hverandre. Det er en god idé å begynne med de lettere oppgavene dersom du ikke er komfortabel med pensum så langt, men det er anbefalt å prøve seg på de vanskeligere oppgavene om du synes de første oppgavene er uproblematiske. Dersom du allerede føler deg trygg på punktene i øvingskravene kan du forsøke å gå rett på de vanskeligere oppgavene. Du er selvfølgelig velkommen til å løse flere oppgaver enn minstekravet, hvilket lurt gjøres med tanke på eksamen og et langt liv som programmerende. + +Før du setter i gang kan det vært lurt å lese nevnte [wikiside om innkapsling](https://www.ntnu.no/wiki/display/tdt4100/Innkapsling) nøye. Forelesningene og tilhørende øvingsforelesning er selvsagt også lure å få med seg. + +### Hjelp / mistanke om bugs + +Ved spørsmål eller behov for hjelp konsulter studassen din i saltiden hans / hennes. Du kan også oppsøke andre studasser på sal eller legge ut et innlegg på [Piazza](https://piazza.com/ntnu.no/spring2025/tdt4100). + +### Godkjenning + +Last opp kildekode på Blackboard innen den angitte innleveringsfristen. Innlevert kode skal demonstreres for stud.ass innen én uke etter innleveringsfrist. Se for øvrig Blackboard-sidene for informasjon rundt organisering av øvingsopplegget og det tilhørende øvingsreglementet. diff --git a/oppgavetekster/oving2/Vehicle.md b/oppgavetekster/oving2/Vehicle.md new file mode 100644 index 0000000..cbbccff --- /dev/null +++ b/oppgavetekster/oving2/Vehicle.md @@ -0,0 +1,30 @@ +# Innkapsling - Vehicle-oppgave + +Oppgaven handler om en `Vehicle`-klasse, som håndterer informasjon om et kjøretøy og implementerer innkapslingsmetoder med validering. + +Et `Vehicle`-objekt inneholder type _kjøretøy_, _drivstoffet_ det bruker og _registreringsnummer_: + +- Typen kan være enten motorsykkel eller bil. +- Drivstoffet kan være enten hydrogen, elektrisitet, diesel eller bensin. Kun biler kan gå på hydrogen. +- Gyldige registreringsnummeret avhenger av typen kjøretøy og drivstoff etter følgende regler: + - Kjøretøy som går på elektrisitet skal ha registreringsnummer som starter med bokstavene `"EL"` eller `"EK"`. + - Hydrogenbilene har registreringsnummer som begynner med bokstavene `"HY"`. + - Dieselkjøretøy og bensinkjøretøy har registreringsnummer som begynner på to bokstaver. De kan ikke være `"EK"`, `"EL"` eller `"HY"`. Bokstavene Æ, Ø og Å skal ikke brukes. + - For alle drivstoff gjelder det at det skal være brukt store bokstaver. + - Ellers så gjelder det at motorsykler har 4 sifre etter bokstavene, mens biler har 5. + +Følgende metoder må implementeres: + +- `Vehicle(char, char, String)` - Konstruktør der argument-rekkefølgen må være kjøretøystype, drivstofftype og registreringsnummer. Ved ugyldige argumenter utløses unntak av typen `IllegalArgumentException`. +- `char getFuelType()` - returnerer type drivstoff som følgende: `'H'` for hydrogen, `'E'` for elektrisitet, `'D'` for diesel eller `'G'` for bensin. +- `String getRegistrationNumber()` - returnerer registreringsnummeret. +- `void setRegistrationNumber(String)` - endrer registreringsnummeret dersom det er gyldig i henhold til kravene over, og utløser unntak av typen `IllegalArgumentException` dersom det ikke er gyldig. +- `char getVehicleType()` - returnerer kjøretøystype: `'M'` for motorsykkel, `'C'` for bil. + +## Java-kode + +Implementer `Vehicle`-klassen som beskrevet over med stram innkapsling. Eventuelle hjelpemetoder for validering bør også ha stram innkapsling. Det kan være lurt å lese om [String-klassen](https://www.ntnu.no/wiki/display/tdt4100/java.lang.String) og dens metoder før du setter i gang. + +Testkode for denne oppgaven finner du i [oving2/VehicleTest.java](../../src/test/java/oving2/VehicleTest.java). + +Merk at din implementasjon må ligge i en pakke med samme navn som testkodens pakke. Pass derfor på at Vehicle-klassen ligger i pakken `oving2`. diff --git a/src/main/java/oving2/.gitkeep b/src/main/java/oving2/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/oving2/AccountTest.java b/src/test/java/oving2/AccountTest.java new file mode 100644 index 0000000..8c2696b --- /dev/null +++ b/src/test/java/oving2/AccountTest.java @@ -0,0 +1,68 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class AccountTest { + + private static final double epsilon = 0.000001; + + private Account account; + + @BeforeEach + public void setUp() { + account = new Account(100, 5); + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(Account.class); + } + + @Test + public void testConstructor() { + assertEquals(100.0d, account.getBalance(), epsilon); + assertEquals(5.0d, account.getInterestRate(), epsilon); + + assertThrows(IllegalArgumentException.class, () -> { + new Account(-1, 5); + }); + assertThrows(IllegalArgumentException.class, () -> { + new Account(100, -1); + }); + } + + @Test + public void testSetInterestRate() { + account.setInterestRate(7); + assertEquals(7, account.getInterestRate(), epsilon); + + assertThrows(IllegalArgumentException.class, () -> { + account.setInterestRate(-2); + }); + } + + @Test + public void testDeposit() { + account.deposit(100); + assertEquals(200.0d, account.getBalance(), epsilon); + + assertThrows(IllegalArgumentException.class, () -> { + account.deposit(-50); + }); + } + + @Test + public void testWithdraw() { + account.withdraw(50); + assertEquals(50.0d, account.getBalance(), epsilon); + + assertThrows(IllegalArgumentException.class, () -> { + account.withdraw(150); + }); + } +} diff --git a/src/test/java/oving2/DigitTest.java b/src/test/java/oving2/DigitTest.java new file mode 100644 index 0000000..62f2413 --- /dev/null +++ b/src/test/java/oving2/DigitTest.java @@ -0,0 +1,75 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class DigitTest { + + private Digit digit; + + private void testIncrement(int base, boolean checkValue, boolean checkToString) { + digit = new Digit(base); + int i = 0; + String digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + while (i < base) { + if (checkValue) { + assertEquals(i % base, digit.getValue()); + } + if (checkToString) { + assertEquals(String.valueOf(digits.charAt(i % base)), + digit.toString().toUpperCase()); + } + + boolean overflow = digit.increment(); + i++; + + if (checkValue) { + assertEquals(i % base == 0, overflow); + assertEquals(i % base, digit.getValue()); + } + if (checkToString) { + assertEquals(String.valueOf(digits.charAt(i % base)), + digit.toString().toUpperCase()); + } + } + } + + private void testIncrement(boolean checkValue, boolean checkToString) { + for (int base = 2; base <= 16; base++) { + this.testIncrement(base, checkValue, checkToString); + } + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(Digit.class); + } + + @Test + public void testDigit() { + digit = new Digit(10); + assertEquals(0, digit.getValue()); + assertEquals(10, digit.getBase()); + + assertThrows(IllegalArgumentException.class, () -> { + digit = new Digit(-1); + }, "Should not be able to create digit with negative base"); + } + + @Test + @DisplayName("Digit should increment correctly") + public void testIncrement() { + this.testIncrement(true, false); + } + + @Test + @DisplayName("Digit should be displayed by correct characters. For example 10 in base 16 " + + "should be displayed as A") + public void testToString() { + this.testIncrement(false, true); + } +} diff --git a/src/test/java/oving2/LineEditorTest.java b/src/test/java/oving2/LineEditorTest.java new file mode 100644 index 0000000..86ea09a --- /dev/null +++ b/src/test/java/oving2/LineEditorTest.java @@ -0,0 +1,196 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LineEditorTest { + + private LineEditor lineEditor; + + @BeforeEach + public void setUp() { + lineEditor = new LineEditor(); + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(LineEditor.class); + } + + @Test + public void testConstructor() { + assertEquals("", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + } + + @Test + public void testGetSetText() { + lineEditor.setText("ABC"); + assertEquals("ABC", lineEditor.getText()); + assertEquals(3, lineEditor.getInsertionIndex()); + + lineEditor.setText(""); + assertEquals("", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + assertThrows(IllegalArgumentException.class, () -> { + lineEditor.setText(null); + }, "Cannot set text to null"); + } + + @Test + public void testGetSetInsertionIndex() { + lineEditor.setText("ABC"); + assertEquals("ABC", lineEditor.getText()); + assertEquals(3, lineEditor.getInsertionIndex()); + + lineEditor.setInsertionIndex(0); + assertEquals("ABC", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.setInsertionIndex(3); + assertEquals("ABC", lineEditor.getText()); + assertEquals(3, lineEditor.getInsertionIndex()); + + assertThrows(IllegalArgumentException.class, () -> { + lineEditor.setInsertionIndex(-1); + }); + + assertThrows(IllegalArgumentException.class, () -> { + lineEditor.setInsertionIndex(4); + }); + } + + @Test + public void testLeft() { + lineEditor.setText("Ja"); + lineEditor.setInsertionIndex(2); + lineEditor.left(); + assertEquals("Ja", lineEditor.getText()); + assertEquals(1, lineEditor.getInsertionIndex()); + + lineEditor.left(); + assertEquals("Ja", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.left(); + assertEquals("Ja", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + } + + @Test + public void testRight() { + lineEditor.setText("Ja"); + lineEditor.setInsertionIndex(0); + lineEditor.right(); + assertEquals("Ja", lineEditor.getText()); + assertEquals(1, lineEditor.getInsertionIndex()); + + lineEditor.right(); + lineEditor.setText("Ja"); + assertEquals(2, lineEditor.getInsertionIndex()); + + lineEditor.right(); + lineEditor.setText("Ja"); + assertEquals(2, lineEditor.getInsertionIndex()); + } + + @Test + public void testDeleteLeft() { + lineEditor.setText("Ja"); + lineEditor.setInsertionIndex(0); + lineEditor.deleteLeft(); + assertEquals("Ja", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.setInsertionIndex(1); + lineEditor.deleteLeft(); + assertEquals("a", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.deleteLeft(); + assertEquals("a", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.setText("Ja"); + lineEditor.setInsertionIndex(2); + lineEditor.deleteLeft(); + assertEquals("J", lineEditor.getText()); + assertEquals(1, lineEditor.getInsertionIndex()); + + lineEditor.deleteLeft(); + assertEquals("", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.deleteLeft(); + assertEquals("", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + } + + @Test + public void testDeleteRight() { + lineEditor.setText("Ja"); + lineEditor.setInsertionIndex(2); + lineEditor.deleteRight(); + assertEquals("Ja", lineEditor.getText()); + assertEquals(2, lineEditor.getInsertionIndex()); + + lineEditor.setInsertionIndex(1); + lineEditor.deleteRight(); + assertEquals("J", lineEditor.getText()); + assertEquals(1, lineEditor.getInsertionIndex()); + + lineEditor.deleteRight(); + assertEquals("J", lineEditor.getText()); + assertEquals(1, lineEditor.getInsertionIndex()); + + lineEditor.setText("Ja"); + lineEditor.setInsertionIndex(0); + lineEditor.deleteRight(); + assertEquals("a", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.deleteRight(); + assertEquals("", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.deleteRight(); + assertEquals("", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + } + + @Test + public void testInsertString() { + lineEditor.insertString(""); + assertEquals("", lineEditor.getText()); + assertEquals(0, lineEditor.getInsertionIndex()); + + lineEditor.insertString("Java"); + assertEquals("Java", lineEditor.getText()); + assertEquals(4, lineEditor.getInsertionIndex()); + + lineEditor.insertString(" er gøy!"); + assertEquals("Java er gøy!", lineEditor.getText()); + assertEquals(12, lineEditor.getInsertionIndex()); + + lineEditor.setText("Javagøy!"); + lineEditor.setInsertionIndex(4); + lineEditor.insertString(" er "); + assertEquals("Java er gøy!", lineEditor.getText()); + assertEquals(8, lineEditor.getInsertionIndex()); + + lineEditor.setText("er gøy!"); + lineEditor.setInsertionIndex(0); + lineEditor.insertString("Java "); + assertEquals("Java er gøy!", lineEditor.getText()); + assertEquals(5, lineEditor.getInsertionIndex()); + + assertThrows(IllegalArgumentException.class, () -> { + lineEditor.insertString(null); + }, "Cannot insert null"); + } +} diff --git a/src/test/java/oving2/LocationTest.java b/src/test/java/oving2/LocationTest.java new file mode 100644 index 0000000..e62e138 --- /dev/null +++ b/src/test/java/oving2/LocationTest.java @@ -0,0 +1,107 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LocationTest { + + private Location loc; + + /** + * Check that the position of {@link #loc} is equal to the parameters. + * + * @param x Expected x position + * @param y Expected y position + */ + private void checkPos(int x, int y) { + assertEquals(x, loc.getX(), "Wrong x coordinate"); + assertEquals(y, loc.getY(), "Wrong y coordinate"); + } + + @BeforeEach + public void beforeEach() { + loc = new Location(); + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(Location.class); + } + + @Test + @DisplayName("Constructor") + public void testConstructor() { + this.checkPos(0, 0); + } + + @Test + @DisplayName("Move up") + public void testUp() { + loc.up(); + this.checkPos(0, -1); + + loc.up(); + this.checkPos(0, -2); + } + + @Test + @DisplayName("Move down") + public void testDown() { + loc.down(); + this.checkPos(0, 1); + + loc.down(); + this.checkPos(0, 2); + } + + @Test + @DisplayName("Move left") + public void testLeft() { + loc.left(); + this.checkPos(-1, 0); + + loc.left(); + this.checkPos(-2, 0); + } + + @Test + @DisplayName("Move right") + public void testRight() { + loc.right(); + this.checkPos(1, 0); + + loc.right(); + this.checkPos(2, 0); + } + + @Test + @DisplayName("Move multiple directions") + public void testComplexMovement() { + loc.right(); + this.checkPos(1, 0); + + loc.down(); + this.checkPos(1, 1); + + loc.right(); + this.checkPos(2, 1); + + loc.down(); + this.checkPos(2, 2); + + loc.left(); + this.checkPos(1, 2); + + loc.up(); + this.checkPos(1, 1); + + loc.up(); + this.checkPos(1, 0); + + loc.left(); + this.checkPos(0, 0); + } +} diff --git a/src/test/java/oving2/PersonTest.java b/src/test/java/oving2/PersonTest.java new file mode 100644 index 0000000..d6faa75 --- /dev/null +++ b/src/test/java/oving2/PersonTest.java @@ -0,0 +1,221 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.Date; +import java.util.Random; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PersonTest { + + private Person person; + + private void testInvalidName(String invalidName, String existingName) { + assertThrows(IllegalArgumentException.class, () -> { + person.setName(invalidName); + }); + + assertEquals(existingName, person.getName()); + } + + private void testInvalidEmail(String invalidEmail, String existingEmail, + Class ex) { + assertThrows(ex, () -> { + person.setEmail(invalidEmail); + }); + + assertEquals(existingEmail, person.getEmail()); + } + + private static String generateValidDomain() { + Random random = new Random(); + int length = random.nextInt(63) + 1; + String validCharacters = "abcdefghijklmnopqrstuvwxyz0123456789"; + String domain = ""; + + for (int currentChar = 0; currentChar < length; currentChar++) { + int character = random.nextInt(36); + domain += validCharacters.substring(character, character + 1); + } + + return domain; + } + + @BeforeEach + public void setUp() { + person = new Person(); + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(Person.class); + } + + @Test + public void testSetName() { + String name = person.getName(); + this.testInvalidName("Ola", name); + this.testInvalidName("O N", name); + this.testInvalidName("Ola Mellom Nordmann", name); + this.testInvalidName("O. Nordmann", name); + + assertDoesNotThrow(() -> { + person.setName("Espen Askeladd"); + }); + assertThrows(IllegalArgumentException.class, () -> { + person.setName(null); + }, "Name cannot be null"); + + assertEquals("Espen Askeladd", person.getName()); + } + + @Test + public void testSetBirthday() { + long today = new Date().getTime(); + long offset = 1000L * 60L * 60L * 24L * 100L; // About 100 days + + // Test with null birthday + assertThrows(IllegalArgumentException.class, () -> { + person.setBirthday(null); + }, "Birthday cannot be null"); + + // Test with incorrect birthday + assertThrows(IllegalArgumentException.class, () -> { + Date theFuture = new Date(today + offset); + person.setBirthday(theFuture); + }); + + // Test with correct birthday + Date thePast = new Date(today - offset); + + assertDoesNotThrow(() -> { + person.setBirthday(thePast); + }); + + assertEquals(thePast, person.getBirthday()); + } + + @Test + public void testSetEmail() { + assertThrows(IllegalStateException.class, () -> { + person.setEmail("ola.nordmann@ntnu.no"); + }, "Email cannot be set before name"); + + person.setName("Ola Nordmann"); + String email = person.getEmail(); + this.testInvalidEmail("ola.nordmann@ntnu", email, IllegalArgumentException.class); + this.testInvalidEmail("ola.nordmann(at)ntnu.no", email, IllegalArgumentException.class); + this.testInvalidEmail("espen.askeladd@eventyr.no", email, IllegalArgumentException.class); + this.testInvalidEmail(null, email, IllegalArgumentException.class); + + assertDoesNotThrow(() -> { + person.setEmail("ola.nordmann@ntnu.no"); + }); + + assertEquals("ola.nordmann@ntnu.no", person.getEmail()); + + assertThrows(IllegalStateException.class, () -> { + person.setName("Untz Untz"); + }, "Name cannot be set after email"); + } + + @Test + public void testExtraCountryTopLevelDomains() { + String[] cTLDs = {"ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", "at", + "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", + "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", + "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cw", "cx", + "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", + "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", + "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", + "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", + "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", + "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", + "md", "me", "mf", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", + "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", + "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", + "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", + "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", + "st", "sv", "sx", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", + "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "um", "us", "uy", "uz", "va", + "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "zw"}; + String[] invalidCTLDs = {"aa", "ab", "ac", "ah", "aj", "ak", "an", "ap", "av", "ay", "bc", + "bk", "bp", "bu", "bx", "cb", "ce", "cj", "cp", "cq", "cs", "ct", "da", "db", "dc", + "dd", "df", "dg", "dh", "di", "dl", "dn", "dp", "dq", "dr", "ds", "dt", "du", "dv", + "dw", "dx", "dy", "ea", "eb", "ed", "ef", "ei", "ej", "ek", "el", "em", "en", "eo", + "ep", "eq", "eu", "ev", "ew", "ex", "ey", "ez", "fa", "fb", "fc", "fd", "fe", "ff", + "fg", "fh", "fl", "fn", "fp", "fq", "fs", "ft", "fu", "fv", "fw", "fx", "fy", "fz", + "gc", "gj", "gk", "go", "gv", "gx", "gz", "ha", "hb", "hc", "hd", "he", "hf", "hg", + "hh", "hi", "hj", "hl", "ho", "hp", "hq", "hs", "hv", "hw", "hx", "hy", "hz", "ia", + "ib", "ic", "if", "ig", "ih", "ii", "ij", "ik", "ip", "iu", "iv", "iw", "ix", "iy", + "iz", "ja", "jb", "jc", "jd", "jf", "jg", "jh", "ji", "jj", "jk", "jl", "jn", "jq", + "jr", "js", "jt", "ju", "jv", "jw", "jx", "jy", "jz", "ka", "kb", "kc", "kd", "kf", + "kj", "kk", "kl", "ko", "kq", "ks", "kt", "ku", "kv", "kx", "ld", "le", "lf", "lg", + "lh", "lj", "ll", "lm", "ln", "lo", "lp", "lq", "lw", "lx", "lz", "mb", "mi", "mj", + "nb", "nd", "nh", "nj", "nk", "nm", "nn", "nq", "ns", "nt", "nv", "nw", "nx", "ny", + "oa", "ob", "oc", "od", "oe", "of", "og", "oh", "oi", "oj", "ok", "ol", "on", "oo", + "op", "oq", "or", "os", "ot", "ou", "ov", "ow", "ox", "oy", "oz", "pb", "pc", "pd", + "pi", "pj", "po", "pp", "pq", "pu", "pv", "px", "pz", "qb", "qc", "qd", "qe", "qf", + "qg", "qh", "qi", "qj", "qk", "ql", "qm", "qn", "qo", "qp", "qq", "qr", "qs", "qt", + "qu", "qv", "qw", "qx", "qy", "qz", "ra", "rb", "rc", "rd", "rf", "rg", "rh", "ri", + "rj", "rk", "rl", "rm", "rn", "rp", "rq", "rr", "rt", "rv", "rx", "ry", "rz", "sf", + "sp", "sq", "su", "sw", "ta", "tb", "te", "ti", "tp", "tq", "ts", "tu", "tx", "ty", + "ub", "uc", "ud", "ue", "uf", "uh", "ui", "uj", "uk", "ul", "un", "uo", "up", "uq", + "ur", "ut", "uu", "uv", "uw", "ux", "vb", "vd", "vf", "vh", "vj", "vk", "vl", "vm", + "vo", "vp", "vq", "vr", "vs", "vt", "vv", "vw", "vx", "vy", "vz", "wa", "wb", "wc", + "wd", "we", "wg", "wh", "wi", "wj", "wk", "wl", "wm", "wn", "wo", "wp", "wq", "wr", + "wt", "wu", "wv", "ww", "wx", "wy", "wz", "xa", "xb", "xc", "xd", "xe", "xf", "xg", + "xh", "xi", "xj", "xk", "xl", "xm", "xn", "xo", "xp", "xq", "xr", "xs", "xt", "xu", + "xv", "xw", "xx", "xy", "xz", "ya", "yb", "yc", "yd", "yf", "yg", "yh", "yi", "yj", + "yk", "yl", "ym", "yn", "yo", "yp", "yq", "yr", "ys", "yu", "yv", "yw", "yx", "yy", + "yz", "zb", "zc", "zd", "ze", "zf", "zg", "zh", "zi", "zj", "zk", "zl", "zn", "zo", + "zp", "zq", "zr", "zs", "zt", "zu", "zv", "zx", "zy", "zz"}; + + person.setName("John Doe"); + String email = person.getEmail(); + + for (String cTLD : invalidCTLDs) { + this.testInvalidEmail("john.doe@ntnu." + cTLD, email, IllegalArgumentException.class); + } + + for (String cTLD : cTLDs) { + String localemail = "john.doe@" + PersonTest.generateValidDomain() + "." + cTLD; + + assertDoesNotThrow(() -> { + person.setEmail(localemail); + }); + + assertEquals(localemail, person.getEmail()); + } + } + + @Test + public void testSetGender() { + String validGenders = "FM\0"; + char gender = person.getGender(); + + for (char c = '\0'; c < '\uFFFF'; c++) { + char localc = c; + + if (validGenders.indexOf(c) < 0) { + gender = person.getGender(); + + assertThrows(IllegalArgumentException.class, () -> { + person.setGender(localc); + }); + + assertEquals(gender, person.getGender()); + } else { + assertDoesNotThrow(() -> { + person.setGender(localc); + }); + + assertEquals(localc, person.getGender()); + } + } + } +} diff --git a/src/test/java/oving2/PersonTest2.java b/src/test/java/oving2/PersonTest2.java new file mode 100644 index 0000000..b51f3a8 --- /dev/null +++ b/src/test/java/oving2/PersonTest2.java @@ -0,0 +1,97 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class PersonTest2 { + + private static final int[] factors1 = {3, 7, 6, 1, 8, 9, 4, 5, 2}; + private static final int[] factors2 = {5, 4, 3, 2, 7, 6, 5, 4, 3, 2}; + + private Person person; + + @BeforeEach + public void setUp() { + person = new Person(); + } + + private static String generateValid(int n1, int n2, int n3, String birthday) { + birthday = birthday + n1 + n2 + n3; + int k1 = 0, k2 = 0; + + for (int i = 0; i < birthday.length(); i++) { + int num = Character.getNumericValue(birthday.charAt(i)); + k1 += factors1[i] * num; + k2 += factors2[i] * num; + } + + k1 = 11 - (k1 % 11); + + if (k1 == 11) { + k1 = 0; + } + + k2 += k1 * factors2[9]; + k2 = 11 - (k2 % 11); + + if (k2 == 11) { + k2 = 0; + } + + return k1 + "" + k2; + } + + @Test + public void testSetSSN() { + LocalDate localDate = LocalDate.of(1994, 1, 1); + Date date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + + person.setBirthday(date); + person.setGender('M'); + + assertThrows(IllegalArgumentException.class, () -> { + person.setSSN(null); + }, "SSN cannot be null"); + + assertDoesNotThrow(() -> { + person.setSSN("010194111" + PersonTest2.generateValid(1, 1, 1, "010194")); + }); + + assertEquals("01019411156", person.getSSN()); + + assertThrows(IllegalArgumentException.class, () -> { + person.setSSN("010194112" + PersonTest2.generateValid(1, 1, 2, "010194")); + }); + + assertEquals("01019411156", person.getSSN()); + + assertThrows(IllegalArgumentException.class, () -> { + person.setSSN("01019411122"); + }); + + assertEquals("01019411156", person.getSSN()); + } + + @Test + public void testModifyingState() { + LocalDate localDate = LocalDate.of(1994, 1, 1); + Date date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + + person.setBirthday(date); + person.setGender('M'); + person.setSSN("010194111" + PersonTest2.generateValid(1, 1, 1, "010194")); + + assertThrows(IllegalStateException.class, () -> { + person.setGender('F'); + }); + assertThrows(IllegalStateException.class, () -> { + person.setBirthday(new Date()); + }); + } +} diff --git a/src/test/java/oving2/StopWatchTest.java b/src/test/java/oving2/StopWatchTest.java new file mode 100644 index 0000000..f521e7b --- /dev/null +++ b/src/test/java/oving2/StopWatchTest.java @@ -0,0 +1,179 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class StopWatchTest { + + private StopWatch stopWatch; + + @BeforeEach + public void beforeEach() { + stopWatch = new StopWatch(); + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(StopWatch.class); + } + + @Test + @DisplayName("Constructor") + public void testConstructor() { + assertFalse(stopWatch.isStarted(), "Stopwatch should not be started"); + assertFalse(stopWatch.isStopped(), "Stopwatch should not be stopped"); + assertEquals(0, stopWatch.getTicks(), "Wrong ticks returned"); + assertEquals(-1, stopWatch.getTime(), "Time should be -1 when not started"); + assertEquals(-1, stopWatch.getLapTime(), "Lap time should be -1 when not started"); + assertEquals(-1, stopWatch.getLastLapTime(), "Last lap time should be -1 when not started"); + } + + @Test + @DisplayName("Tick without starting") + public void testTicksWithoutStart() { + stopWatch.tick(1); + assertEquals(-1, stopWatch.getTime(), "Time should be -1 when not started"); + assertEquals(1, stopWatch.getTicks(), "Ticks should be 1 after calling #tick(1)"); + + stopWatch.tick(4); + assertEquals(-1, stopWatch.getTime(), "Time should be -1 when not started"); + assertEquals(5, stopWatch.getTicks(), "Ticks should be 5 after calling #tick(4)"); + } + + @Test + @DisplayName("Tick, start and stop 1") + public void testStartTickStop() { + stopWatch.start(); + assertEquals(0, stopWatch.getTime(), "Time should be 0 when just started"); + assertEquals(0, stopWatch.getTicks(), "Ticks should be 0 when #tick() has not been called"); + assertTrue(stopWatch.isStarted(), "Should be started after calling #start()"); + assertFalse(stopWatch.isStopped(), "Should not be stopped before calling #stop()"); + + assertThrows(IllegalStateException.class, () -> { + stopWatch.start(); + }, "Cannot start already running stopwatch"); + + stopWatch.tick(3); + assertEquals(3, stopWatch.getTime(), + "Time should be 3 when started and #tick(3) has been called"); + assertEquals(3, stopWatch.getTicks(), "Ticks should be 3 when #tick(3) has been called"); + assertTrue(stopWatch.isStarted(), "Should be started after calling #start()"); + assertFalse(stopWatch.isStopped(), "Should not be stopped before calling #stop()"); + + stopWatch.tick(5); + assertEquals(8, stopWatch.getTime(), + "Time should be 8 when started and #tick(5) has been called"); + assertEquals(8, stopWatch.getTicks(), "Ticks should be 8 when #tick(5) has been called"); + assertTrue(stopWatch.isStarted(), "Should be started after calling #start()"); + assertFalse(stopWatch.isStopped(), "Should not be stopped before calling #stop()"); + + stopWatch.stop(); + assertEquals(8, stopWatch.getTime(), "Time should be 8 after #stop() has been called"); + assertEquals(8, stopWatch.getTicks(), "Ticks should be 8 after #stop() has been called"); + assertTrue(stopWatch.isStarted(), "Should be started even after #stop() has been called"); + assertTrue(stopWatch.isStopped(), "Should be stopped after calling #stop()"); + + assertThrows(IllegalStateException.class, () -> { + stopWatch.stop(); + }, "Cannot stop stopped stopwatch"); + } + + @Test + @DisplayName("Tick, start and stop 2") + public void testTickStartTickStopTick() { + stopWatch.tick(2); + assertEquals(-1, stopWatch.getTime(), "Time should be -1 when not started"); + assertEquals(2, stopWatch.getTicks(), "Ticks should be 2 when #tick(2) has been called"); + assertFalse(stopWatch.isStarted(), "Stopwatch should not be started"); + assertFalse(stopWatch.isStopped(), "Stopwatch should not be stopped"); + + stopWatch.start(); + assertEquals(0, stopWatch.getTime(), "Time should be 0 when just started"); + assertEquals(2, stopWatch.getTicks(), "Ticks should be 2 after #start() has been called"); + assertTrue(stopWatch.isStarted(), "Should be started after calling #start()"); + assertFalse(stopWatch.isStopped(), "Should not be stopped before calling #stop()"); + + stopWatch.tick(3); + assertEquals(3, stopWatch.getTime(), + "Time should be 3 when started and #tick(3) has been called"); + assertEquals(5, stopWatch.getTicks(), "Ticks should be 5 when #tick(3) has been called"); + assertTrue(stopWatch.isStarted(), "Should be started after calling #tick(3)"); + assertFalse(stopWatch.isStopped(), "Should not be stopped before calling #tick(3)"); + + stopWatch.tick(5); + assertEquals(8, stopWatch.getTime(), + "Time should be 8 when started and #tick(5) has been called"); + assertEquals(10, stopWatch.getTicks(), "Ticks should be 10 when #tick(5) has been called"); + assertTrue(stopWatch.isStarted(), "Should be started after calling #tick(5)"); + assertFalse(stopWatch.isStopped(), "Should not be stopped before calling #tick(5)"); + + stopWatch.stop(); + assertEquals(8, stopWatch.getTime(), "Time should be 8 after #stop() has been called"); + assertEquals(10, stopWatch.getTicks(), "Ticks should be 10 after #stop() has been called"); + assertTrue(stopWatch.isStarted(), "Should be started even after #stop() has been called"); + assertTrue(stopWatch.isStopped(), "Should be stopped after calling #stop()"); + + stopWatch.tick(3); + assertEquals(8, stopWatch.getTime(), + "Time should be 8 after #tick(3) has been called while stopped"); + assertEquals(13, stopWatch.getTicks(), + "Ticks should be 13 when #tick(3) has been called while stopped"); + assertTrue(stopWatch.isStarted(), + "Should be started even after #tick() has been called while stopped"); + assertTrue(stopWatch.isStopped(), "Should be stopped after calling #tick() while stopped"); + + assertThrows(IllegalArgumentException.class, () -> { + stopWatch.tick(-1); + }, "Time should not go backwards"); + } + + @Test + @DisplayName("Lap times") + public void testLaps() { + assertThrows(IllegalStateException.class, () -> { + stopWatch.lap(); + }, "Should not be able to lap non-started timer"); + + stopWatch.start(); + assertEquals(0, stopWatch.getTime(), "Time should be 0 when just started"); + assertEquals(0, stopWatch.getLapTime(), "Lap time should be 0 when just started"); + assertEquals(-1, stopWatch.getLastLapTime(), + "Last lap time should be -1 when there is no previous lap"); + + stopWatch.tick(3); + assertEquals(3, stopWatch.getTime(), "Time should be 3 after #tick(3) has been called"); + assertEquals(3, stopWatch.getLapTime(), + "Lap time should be 3 after calling #tick(3) while started"); + assertEquals(-1, stopWatch.getLastLapTime(), + "Last lap time should be -1 when there is no previous lap"); + + stopWatch.lap(); + assertEquals(3, stopWatch.getTime(), "Time should still be 3 after starting a new lap"); + assertEquals(0, stopWatch.getLapTime(), "Current lap time should be 0 when just started"); + assertEquals(3, stopWatch.getLastLapTime(), + "Last lap time should be 3 when we just started a new lap"); + + stopWatch.tick(5); + assertEquals(8, stopWatch.getTime(), "Time should be 8 after #tick(5) has been called"); + assertEquals(5, stopWatch.getLapTime(), + "Current lap time should be 5 after calling #tick(5)"); + assertEquals(3, stopWatch.getLastLapTime(), + "Last lap time should be 3 even after time passes"); + + stopWatch.stop(); + assertEquals(8, stopWatch.getTime(), "Time should be 8 after stopping"); + assertEquals(0, stopWatch.getLapTime(), "Current lap time should be 0 when stopped"); + assertEquals(5, stopWatch.getLastLapTime(), + "Last lap should be the lap time of the current lap when stopping"); + + assertThrows(IllegalStateException.class, () -> { + stopWatch.lap(); + }, "Should not be able to lap stopped timer"); + } +} diff --git a/src/test/java/oving2/TestHelper.java b/src/test/java/oving2/TestHelper.java new file mode 100644 index 0000000..56967d7 --- /dev/null +++ b/src/test/java/oving2/TestHelper.java @@ -0,0 +1,15 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class TestHelper { + + public static void checkIfFieldsPrivate(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + assertTrue(Modifier.isPrivate(field.getModifiers()), + "Expected field " + field.getName() + " to be private!"); + } + } +} diff --git a/src/test/java/oving2/UpOrDownCounterTest.java b/src/test/java/oving2/UpOrDownCounterTest.java new file mode 100644 index 0000000..bb1e798 --- /dev/null +++ b/src/test/java/oving2/UpOrDownCounterTest.java @@ -0,0 +1,70 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class UpOrDownCounterTest { + + private static void testCount(UpOrDownCounter counter, int end, int delta) { + boolean result = true; + + while (delta > 0 ? counter.getCounter() < end : counter.getCounter() > end) { + assertTrue(result, + "Before reaching the end value, the count() method should return true"); + + int i = counter.getCounter(); + result = counter.count(); + int expected = i + delta; + assertEquals(expected, counter.getCounter(), + "When counting from " + i + " the result should be " + expected + ". "); + } + + assertFalse(result, "When reaching the end value, the count method should return false"); + assertEquals(end, counter.getCounter(), + "After reaching the end value, the counter should not change"); + assertFalse(counter.count(), + "After reaching the end value, the count() method should return false"); + assertEquals(end, counter.getCounter(), + "After reaching the end value, the counter should not change"); + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(UpOrDownCounter.class); + } + + @Test + public void testUpOrDownCounter() { + UpOrDownCounter counter13 = new UpOrDownCounter(1, 3); + assertEquals(1, counter13.getCounter(), + "After instantiating an UpOrDownCounter the counter should be the start value"); + + UpOrDownCounter counter31 = new UpOrDownCounter(3, 1); + assertEquals(3, counter31.getCounter(), + "After instantiating an UpOrDownCounter the counter should be the start value"); + } + + @Test + public void testUpOrDownCounterWithException() { + assertThrows(IllegalArgumentException.class, () -> { + new UpOrDownCounter(0, 0); + }, "When the start and end values are equal an IllegalArgumentException should be thrown"); + } + + @Test + public void testCountUp() { + UpOrDownCounter counter13 = new UpOrDownCounter(1, 3); + UpOrDownCounterTest.testCount(counter13, 3, 1); + } + + @Test + public void testCountDown() { + UpOrDownCounter counter31 = new UpOrDownCounter(3, 1); + UpOrDownCounterTest.testCount(counter31, 1, -1); + } +} diff --git a/src/test/java/oving2/VehicleTest.java b/src/test/java/oving2/VehicleTest.java new file mode 100644 index 0000000..aac5113 --- /dev/null +++ b/src/test/java/oving2/VehicleTest.java @@ -0,0 +1,200 @@ +package oving2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class VehicleTest { + + private Vehicle vehicle; + + private static void checkVehicleState(char vehicleType, char fuelType, + String registrationNumber, Vehicle vehicle) { + assertEquals(vehicleType, vehicle.getVehicleType()); + assertEquals(fuelType, vehicle.getFuelType()); + assertEquals(registrationNumber, vehicle.getRegistrationNumber()); + } + + private static void checkInvalidConstructor(char vehicleType, char fuelType, + String registrationNumber) { + assertThrows(IllegalArgumentException.class, () -> { + new Vehicle(vehicleType, fuelType, registrationNumber); + }); + } + + private static void checkInvalidsetRegistration(Vehicle vehicle, String originalRegNum, + String newRegNum) { + assertThrows(IllegalArgumentException.class, () -> { + vehicle.setRegistrationNumber(newRegNum); + }); + + assertEquals(originalRegNum, vehicle.getRegistrationNumber()); + } + + @Test + @DisplayName("Private fields") + public void testPrivateFields() { + TestHelper.checkIfFieldsPrivate(Vehicle.class); + } + + @Test + public void testConstructor() { + vehicle = new Vehicle('C', 'D', "BN12345"); + VehicleTest.checkVehicleState('C', 'D', "BN12345", vehicle); + + vehicle = new Vehicle('M', 'E', "EL1234"); + VehicleTest.checkVehicleState('M', 'E', "EL1234", vehicle); + VehicleTest.checkInvalidConstructor('C', 'D', null); + VehicleTest.checkInvalidConstructor('C', 'Y', "BN12345"); + VehicleTest.checkInvalidConstructor('M', 'H', "HY1234"); + VehicleTest.checkInvalidConstructor('P', 'D', "BN12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "A1234"); + VehicleTest.checkInvalidConstructor('C', 'G', "A12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "A123456"); + VehicleTest.checkInvalidConstructor('C', 'G', "A12344"); + VehicleTest.checkInvalidConstructor('C', 'G', "AÆ12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "ab12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "A12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "A1B12345"); + VehicleTest.checkInvalidConstructor('M', 'G', "A1234"); + VehicleTest.checkInvalidConstructor('M', 'G', "A12345"); + VehicleTest.checkInvalidConstructor('M', 'G', "A123"); + VehicleTest.checkInvalidConstructor('M', 'G', "AB12345"); + VehicleTest.checkInvalidConstructor('M', 'G', "ABC1234"); + VehicleTest.checkInvalidConstructor('M', 'G', "ABC12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "AÅ1234"); + VehicleTest.checkInvalidConstructor('C', 'G', "ab1234"); + VehicleTest.checkInvalidConstructor('C', 'G', "EL12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "EK12345"); + VehicleTest.checkInvalidConstructor('C', 'G', "HY12345"); + VehicleTest.checkInvalidConstructor('C', 'D', "EL12345"); + VehicleTest.checkInvalidConstructor('C', 'D', "EK12345"); + VehicleTest.checkInvalidConstructor('C', 'D', "HY12345"); + VehicleTest.checkInvalidConstructor('C', 'H', "EL12345"); + VehicleTest.checkInvalidConstructor('C', 'H', "EK12345"); + VehicleTest.checkInvalidConstructor('C', 'H', "BN12345"); + VehicleTest.checkInvalidConstructor('C', 'E', "HY12345"); + VehicleTest.checkInvalidConstructor('C', 'E', "BN12345"); + VehicleTest.checkInvalidConstructor('M', 'G', "EL1234"); + VehicleTest.checkInvalidConstructor('M', 'G', "EK1234"); + VehicleTest.checkInvalidConstructor('M', 'G', "HY1234"); + VehicleTest.checkInvalidConstructor('M', 'D', "EL1234"); + VehicleTest.checkInvalidConstructor('M', 'D', "EK1234"); + VehicleTest.checkInvalidConstructor('M', 'D', "HY1234"); + VehicleTest.checkInvalidConstructor('M', 'E', "HY1234"); + VehicleTest.checkInvalidConstructor('M', 'E', "BN1234"); + } + + @Test + public void testSetRegistrationNumber() { + vehicle = new Vehicle('C', 'D', "BN12345"); + vehicle.setRegistrationNumber("AB54321"); + VehicleTest.checkVehicleState('C', 'D', "AB54321", vehicle); + + vehicle = new Vehicle('M', 'E', "EK1234"); + vehicle.setRegistrationNumber("EL4321"); + VehicleTest.checkVehicleState('M', 'E', "EL4321", vehicle); + + vehicle = new Vehicle('C', 'D', "BN12345"); + + assertThrows(IllegalArgumentException.class, () -> { + vehicle.setRegistrationNumber("AB654321"); + }); + + VehicleTest.checkVehicleState('C', 'D', "BN12345", vehicle); + + vehicle = new Vehicle('M', 'E', "EL1234"); + + assertThrows(IllegalArgumentException.class, () -> { + vehicle.setRegistrationNumber("HY1234"); + }); + + VehicleTest.checkVehicleState('M', 'E', "EL1234", vehicle); + + vehicle = new Vehicle('C', 'G', "AB12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "A1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "A12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "AB1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "AB123456"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "ABC1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "AÆ12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "ab12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "A1B2345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "A1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "A12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "AB1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "AB123456"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "ABC1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "AÆ12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "ab12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "A1B2345"); + + vehicle = new Vehicle('M', 'G', "AB1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "A1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "A12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "AB123"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "AB12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "ABC1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "ABC12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "AÅ1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "ab1234"); + + vehicle = new Vehicle('C', 'G', "AB12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "EL12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "EK12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "HY12345"); + + vehicle = new Vehicle('C', 'D', "AB12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "EL12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "EK12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "HY12345"); + + vehicle = new Vehicle('C', 'H', "HY12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "EL12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "EK12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "BN12345"); + + vehicle = new Vehicle('C', 'E', "EL12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "HY12345"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), + "BN12345"); + + vehicle = new Vehicle('M', 'G', "AB1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "EL1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "EK1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "HY1234"); + + vehicle = new Vehicle('M', 'D', "AB1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "EL1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "EK1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "HY1234"); + + vehicle = new Vehicle('M', 'E', "EK1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "HY1234"); + VehicleTest.checkInvalidsetRegistration(vehicle, vehicle.getRegistrationNumber(), "BN1234"); + } +}