From 3a31568ffd17e55545e9d562702e22b3a1467dc3 Mon Sep 17 00:00:00 2001 From: Anton Ahlqvist Date: Wed, 14 Jan 2026 12:42:09 +0100 Subject: [PATCH 1/6] Add updated Traveler and Transport tests, based on previous versions --- src/test/java/org/example/TransportTest.java | 29 +++++ src/test/java/org/example/TravelerTest.java | 128 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/test/java/org/example/TransportTest.java create mode 100644 src/test/java/org/example/TravelerTest.java diff --git a/src/test/java/org/example/TransportTest.java b/src/test/java/org/example/TransportTest.java new file mode 100644 index 00000000..71f6c1bc --- /dev/null +++ b/src/test/java/org/example/TransportTest.java @@ -0,0 +1,29 @@ +package org.example; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TransportTest { + + /** + * Testar att rollDistance() alltid returnerar ett värde inom korrekt spann för varje transporttyp. + * Detta är den centrala logiken i Transport eftersom det styr hur långt spelaren kan förflytta sig. + */ + @Test + void rollDistance_returnsValidRange() { + Transport buss = new Transport(TransportType.BUSS, "100"); + Transport train = new Transport(TransportType.TRAIN, "200"); + Transport airplane = new Transport(TransportType.AIRPLANE, "300"); + + for (int i = 0; i < 50; i++) { + int bussRoll = buss.rollDistance(); + int trainRoll = train.rollDistance(); + int airplaneRoll = airplane.rollDistance(); + + assertTrue(bussRoll >= 1 && bussRoll <= 6, "BUSS roll utanför spann: " + bussRoll); + assertTrue(trainRoll >= 2 && trainRoll <= 12, "TRAIN roll utanför spann: " + trainRoll); + assertTrue(airplaneRoll >= 3 && airplaneRoll <= 18, "AIRPLANE roll utanför spann: " + airplaneRoll); + } + } +} diff --git a/src/test/java/org/example/TravelerTest.java b/src/test/java/org/example/TravelerTest.java new file mode 100644 index 00000000..1b4eedf6 --- /dev/null +++ b/src/test/java/org/example/TravelerTest.java @@ -0,0 +1,128 @@ +package org.example; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; + +public class TravelerTest { + + private Location dummyLocation(int x, int y) { + return new Location("Test", LocationType.CITY, null, x, y); + } + + /** + * Testar att konstruktorn för Traveler sätter spelarens startposition korrekt. + *

+ * Skapar en resenär med en viss startposition och kontrollerar att + * playerPosX och playerPosY matchar koordinaterna för startpositionen. + */ + @Test + void constructorPosition() { + Location start = dummyLocation(3, 7); + Traveler traveler = new Traveler("Bob", start); + + assertEquals(3, traveler.getPlayerPosX()); + assertEquals(7, traveler.getPlayerPosY()); + } + + /** + * Testar att en ny resenär inte är på väg när den skapas. + *

+ * Skapar en Traveler på en startposition och kontrollerar att + * isTravelling() returnerar false innan någon resa har startats. + */ + @Test + void notTravellingInitially() { + Traveler traveler = new Traveler("Bob", dummyLocation(0, 0)); + assertFalse(traveler.isTravelling()); + } + + /** + * Testar att resenären börjar en resa korrekt. + *

+ * Kontrollerar att målet och återstående avstånd sätts, + * samt att resenären markeras som "på väg". + */ + @Test + void startJourneySetsTarget() { + Location start = dummyLocation(0, 0); + Location target = dummyLocation(10, 10); + + Traveler traveler = new Traveler("Bob", start); + traveler.startJourney(target, 15); + + assertTrue(traveler.isTravelling()); + assertEquals(15, traveler.getRemainingDistance()); + } + + /** + * Testar att resenären förflyttar sig korrekt under en tur. + *

+ * Kontrollerar att återstående avstånd minskar med rätt antal steg + * och att tur-räknaren ökar med 1 när resan fortsätter. + */ + @Test + void advanceReducesDistance() { + Traveler traveler = new Traveler("Bob", dummyLocation(0, 0)); + traveler.startJourney(dummyLocation(5, 5), 10); + + traveler.advance(3); + + assertEquals(7, traveler.getRemainingDistance()); + assertEquals(1, traveler.getTurnCount()); + assertTrue(traveler.isTravelling()); + } + + /** + * Testar att resan slutförs när resenären förflyttas längre än återstående avstånd. + *

+ * Kontrollerar att återstående avstånd blir 0, att resenären inte längre är "på väg", + * och att resenären hamnar på målet. + */ + @Test + void advanceCompletesJourney() { + Location target = dummyLocation(5, 5); + Traveler traveler = new Traveler("Bob", dummyLocation(0, 0)); + + traveler.startJourney(target, 5); + traveler.advance(10); + + assertEquals(0, traveler.getRemainingDistance()); + assertFalse(traveler.isTravelling()); + assertEquals(target, traveler.getCurrentLocation()); + } + + /** + * Testar att betalning minskar resenärens pengar korrekt. + *

+ * Skapar en resenär med ett visst saldo och drar av en summa. + * Kontrollerar att spelarens pengar minskar med rätt belopp. + */ + @Test + void payReducesMoney() { + Traveler traveler = new Traveler("Bob", dummyLocation(0, 0)); + BigDecimal startMoney = traveler.getMoney(); + + BigDecimal amount = startMoney.divide(BigDecimal.valueOf(2)); + traveler.pay(amount); + + assertEquals(startMoney.subtract(amount), traveler.getMoney()); + } + + /** + * Testar att betalning kastar ett fel om resenären inte har tillräckligt med pengar. + *

+ * Skapar en resenär och försöker ta ut mer pengar än vad som finns. + * Kontrollerar att ett IllegalStateException kastas med rätt felmeddelande. + */ + @Test + void payNotEnoughMoneyThrows() { + Traveler traveler = new Traveler("Bob", dummyLocation(0, 0)); + BigDecimal cost = traveler.getMoney().add(BigDecimal.ONE); + + Exception exception = assertThrows(IllegalStateException.class, () -> traveler.pay(cost)); + assertEquals("not enough money", exception.getMessage()); + } +} From 5a8a6d12a9ff9138307fbc00d79913967f884366 Mon Sep 17 00:00:00 2001 From: Anton Ahlqvist Date: Wed, 14 Jan 2026 16:42:53 +0100 Subject: [PATCH 2/6] Add tests for JourneyService and PlayerEventService --- .../java/org/example/JourneyServiceTest.java | 231 ++++++++++++++++++ .../org/example/PlayerEventServiceTest.java | 99 ++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/test/java/org/example/JourneyServiceTest.java create mode 100644 src/test/java/org/example/PlayerEventServiceTest.java diff --git a/src/test/java/org/example/JourneyServiceTest.java b/src/test/java/org/example/JourneyServiceTest.java new file mode 100644 index 00000000..bfdfd748 --- /dev/null +++ b/src/test/java/org/example/JourneyServiceTest.java @@ -0,0 +1,231 @@ +package org.example; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import org.example.service.JourneyService; +import org.example.service.PlayerEventService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class JourneyServiceTest { + + private EntityManager em; + private PlayerEventService eventService; + private JourneyService service; + + /** + * Initialiserar testmiljön inför varje test. + * Skapar en mockad EntityManager för att simulera databasoperationer, + * och en mockad PlayerEventService för att hantera spelhändelser. + * Dessa används sedan för att skapa en instans av JourneyService. + */ + @BeforeEach + void setUp() { + em = mock(EntityManager.class); + eventService = mock(PlayerEventService.class); + service = new JourneyService(em, eventService); + } + + /** + * Testar att en resenär kan starta en resa korrekt med tillåten transport. + * Kontrollerar att resenären markeras som "på väg" och att Journey-objektet + * kopplas korrekt till resenären, rutten och transporten. + *

+ * Mockito används för att mocka Transport och PlayerEventService, + * vilket gör det möjligt att kontrollera reselogiken utan att slumpen påverkar testet. + */ + @Test + void startJourney() { + Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); + Location to = new Location("Berlin", LocationType.CAPITAL, null, 5, 5); + Traveler traveler = new Traveler("Bob", from); + traveler.addMoney(BigDecimal.valueOf(1000)); + + Transport transport = mock(Transport.class); + when(transport.getCostPerMove()).thenReturn(BigDecimal.valueOf(10)); + when(transport.rollDistance()).thenReturn(1); + when(transport.getType()).thenReturn("BUSS"); + + LocationLink route = new LocationLink(from, to, 6); + route.getTransportLinks().add(new TransportLink(route, transport)); + + PossibleMoves move = new PossibleMoves(route, transport); + when(eventService.applyEndOfTurnEvents(traveler)).thenReturn(List.of()); + + Journey journey = service.startNewJourneyTurn(traveler, move); + + assertTrue(traveler.isTravelling()); + assertTrue(traveler.getRemainingDistance() >= 0); + assertEquals(traveler, journey.getTraveler()); + assertEquals(route, journey.getLocationLink()); + assertEquals(transport, journey.getTransport()); + } + + /** + * Testar att en resenär inte kan starta en ny resa när en pågående resa redan finns. + * Verifierar att startNewJourneyTurn kastar IllegalStateException med korrekt felmeddelande. + * Säkerställer att reselogiken inte tillåter parallella resor för samma resenär. + */ + @Test + void startJourneyWhileTravelling() { + Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); + Location to = new Location("Berlin", LocationType.CAPITAL, null, 5, 5); + Traveler traveler = new Traveler("Bob", from); + traveler.startJourney(to, 6); + + Transport transport = new Transport(TransportType.BUSS, "10"); + LocationLink route = new LocationLink(from, to, 6); + route.getTransportLinks().add(new TransportLink(route, transport)); + PossibleMoves move = new PossibleMoves(route, transport); + + IllegalStateException ex = assertThrows( + IllegalStateException.class, + () -> service.startNewJourneyTurn(traveler, move) + ); + assertEquals("already travelling – cannot start a new journey", ex.getMessage()); + } + + /** + * Testar att det inte går att använda en transport som inte är tillåten på rutten. + * Ska kasta IllegalStateException med korrekt felmeddelande. + */ + @Test + void disallowedTransport() { + Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); + Location to = new Location("Berlin", LocationType.CAPITAL, null, 5, 5); + Traveler traveler = new Traveler("Bob", from); + traveler.addMoney(BigDecimal.valueOf(1000)); + + Transport allowed = new Transport(TransportType.BUSS, "10"); + Transport disallowed = new Transport(TransportType.TRAIN, "15"); + + LocationLink route = new LocationLink(from, to, 6); + route.getTransportLinks().add(new TransportLink(route, allowed)); + PossibleMoves move = new PossibleMoves(route, disallowed); + + IllegalStateException ex = assertThrows( + IllegalStateException.class, + () -> service.startNewJourneyTurn(traveler, move) + ); + assertEquals(disallowed.getType() + " is not allowed on this route", ex.getMessage()); + } + + /** + * Resenärens saldo nollställs och sätts till 5 krediter, vilket är mindre än + * kostnaden för den valda transporten (10 krediter). Vid anrop av + * startNewJourneyTurn med en transport som kostar mer än resenärens saldo + * ska en IllegalStateException kastas med meddelandet + * "traveler cannot afford this move". + */ + @Test + void notEnoughMoney() { + Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); + Location to = new Location("Berlin", LocationType.CAPITAL, null, 5, 5); + Traveler traveler = new Traveler("Bob", from); + + traveler.subtractMoneyClamped(traveler.getMoney()); + + traveler.addMoney(BigDecimal.valueOf(5)); + + Transport expensiveTransport = new Transport(TransportType.BUSS, "10"); + LocationLink route = new LocationLink(from, to, 6); + route.getTransportLinks().add(new TransportLink(route, expensiveTransport)); + + PossibleMoves move = new PossibleMoves(route, expensiveTransport); + + IllegalStateException ex = assertThrows( + IllegalStateException.class, + () -> service.startNewJourneyTurn(traveler, move) + ); + + assertEquals("traveler cannot afford this move", ex.getMessage()); + } + + /** + * Testar att en pågående resa fortsätter korrekt. + * Säkerställer att resenären avancerar, kostnad dras och ett nytt Journey-objekt skapas. + * Kontrollerar att Journey är kopplat till rätt Traveler, Transport och LocationLink, + * samt att resenären fortfarande är markerad som "på väg" och återstående distans är uppdaterad. + *

+ * Mockito används för att mocka EntityManager och PlayerEventService, + * vilket gör det möjligt att testa reselogiken utan att slumpmässiga events påverkar resultatet. + */ + @Test + void continueJourney() { + Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); + Location to = new Location("Berlin", LocationType.CAPITAL, null, 5, 5); + + Traveler traveler = new Traveler("Bob", from); + traveler.addMoney(BigDecimal.valueOf(1000)); + traveler.startJourney(to, 6); + + Transport transport = mock(Transport.class); + when(transport.getCostPerMove()).thenReturn(BigDecimal.valueOf(10)); + when(transport.rollDistance()).thenReturn(1); + when(transport.getType()).thenReturn("BUSS"); + + LocationLink route = new LocationLink(from, to, 6); + route.getTransportLinks().add(new TransportLink(route, transport)); + + Journey lastJourney = new Journey(traveler, route, transport, 0, 6, 0); + TypedQuery queryMock = mock(TypedQuery.class); + when(em.createQuery(anyString(), eq(Journey.class))).thenReturn(queryMock); + when(queryMock.setParameter("t", traveler)).thenReturn(queryMock); + when(queryMock.setMaxResults(1)).thenReturn(queryMock); + when(queryMock.getSingleResult()).thenReturn(lastJourney); + when(eventService.applyEndOfTurnEvents(traveler)).thenReturn(List.of()); + + Journey journey = service.continueCurrentJourneyTurn(traveler); + + assertEquals(traveler, journey.getTraveler()); + assertEquals(route, journey.getLocationLink()); + assertEquals(transport, journey.getTransport()); + assertTrue(traveler.isTravelling()); + assertTrue(traveler.getRemainingDistance() >= 0); + } + + /** + * Skapar en route med flera transporter och mockar EntityManager för att returnera den. + * Kontrollerar att metoden returnerar rätt antal PossibleMoves och att varje move + * är kopplad till rätt route och transport. + */ + @Test + void findMoves() { + Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); + Location to = new Location("Berlin", LocationType.CAPITAL, null, 1, 1); + + Transport bus = new Transport(TransportType.BUSS, "10"); + Transport train = new Transport(TransportType.TRAIN, "20"); + + LocationLink route = new LocationLink(from, to, 5); + route.getTransportLinks().add(new TransportLink(route, bus)); + route.getTransportLinks().add(new TransportLink(route, train)); + + TypedQuery queryMock = mock(TypedQuery.class); + when(em.createQuery(anyString(), eq(LocationLink.class))).thenReturn(queryMock); + when(queryMock.setParameter("location", from)).thenReturn(queryMock); + when(queryMock.getResultList()).thenReturn(List.of(route)); + + List moves = service.findPossibleMoves(from); + + assertEquals(2, moves.size(), "Det ska finnas två möjliga moves"); + + PossibleMoves move1 = moves.get(0); + PossibleMoves move2 = moves.get(1); + + assertTrue(move1.getTransport().equals(bus) || move1.getTransport().equals(train)); + assertTrue(move2.getTransport().equals(bus) || move2.getTransport().equals(train)); + + assertEquals(route, move1.getRoute()); + assertEquals(route, move2.getRoute()); + } +} diff --git a/src/test/java/org/example/PlayerEventServiceTest.java b/src/test/java/org/example/PlayerEventServiceTest.java new file mode 100644 index 00000000..be8bf4f2 --- /dev/null +++ b/src/test/java/org/example/PlayerEventServiceTest.java @@ -0,0 +1,99 @@ +package org.example; + +import org.example.service.PlayerEventService; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PlayerEventServiceTest { + + private Traveler dummyTraveler() { + Traveler t = new Traveler("Bob", new Location("Stockholm", LocationType.CAPITAL, null, 0, 0)); + t.addMoney(BigDecimal.valueOf(1000)); + return t; + } + + /** + * Verifierar att metoden applyEndOfTurnEvents kan köras utan undantag. + * Testet säkerställer att en lista alltid returneras, även om inga events + * genereras, och att metoden inte kraschar när en Traveler skickas in. + */ + @Test + void runWithoutError() { + Traveler traveler = dummyTraveler(); + PlayerEventService service = new PlayerEventService(); + + List events = service.applyEndOfTurnEvents(traveler); + + assertNotNull(events); + } + + /** + * Verifierar att applyEndOfTurnEvents aldrig returnerar fler än två events. + * Testet säkerställer att listan kan innehålla 0, 1 eller 2 EventResult-objekt + * beroende på slumpen, men aldrig fler. + */ + @Test + void eventCountWithinBounds() { + Traveler traveler = dummyTraveler(); + PlayerEventService service = new PlayerEventService(); + + List events = service.applyEndOfTurnEvents(traveler); + + assertTrue(events.size() >= 0 && events.size() <= 2); + } + + /** + * Säkerställer att resenärens saldo aldrig blir negativt efter + * att applyEndOfTurnEvents har körts. Testet verifierar att + * event som drar pengar inte kan göra money < 0. + */ + @Test + void moneyNotNegative() { + Traveler traveler = dummyTraveler(); + PlayerEventService service = new PlayerEventService(); + + service.applyEndOfTurnEvents(traveler); + + assertTrue(traveler.getMoney().compareTo(BigDecimal.ZERO) >= 0); + } + + /** + * Verifierar att alla genererade events har korrekt EventType + * (BONUS eller PENALTY) och att beloppet alltid är positivt. + * Säkerställer att applyEndOfTurnEvents aldrig skapar ogiltiga events. + */ + @Test + void eventsHaveValidTypeAndAmount() { + Traveler traveler = dummyTraveler(); + PlayerEventService service = new PlayerEventService(); + + List events = service.applyEndOfTurnEvents(traveler); + + for (PlayerEventService.EventResult e : events) { + assertTrue(e.type() == EventType.BONUS || e.type() == EventType.PENALTY); + assertTrue(e.amount().compareTo(BigDecimal.ZERO) > 0); + } + } + + /** + * Säkerställer att alla EventResult som genereras av + * applyEndOfTurnEvents innehåller ett meddelande. + * Testet verifierar att message aldrig är null, även om inga events skapas. + */ + @Test + void eventsHaveMessage() { + Traveler traveler = dummyTraveler(); + PlayerEventService service = new PlayerEventService(); + + List events = service.applyEndOfTurnEvents(traveler); + + for (PlayerEventService.EventResult e : events) { + assertNotNull(e.message()); + } + } +} From 8b0470919eb63b0b6d40703ee7a271ff38e59505 Mon Sep 17 00:00:00 2001 From: Anton Ahlqvist Date: Wed, 14 Jan 2026 18:49:55 +0100 Subject: [PATCH 3/6] feat: improve JourneyService tests after rabbit suggestions and add BootstrapService tests --- .../org/example/BootstrapServiceTest.java | 103 ++++++++++++++++++ .../java/org/example/JourneyServiceTest.java | 21 +++- 2 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/example/BootstrapServiceTest.java diff --git a/src/test/java/org/example/BootstrapServiceTest.java b/src/test/java/org/example/BootstrapServiceTest.java new file mode 100644 index 00000000..df6bba7c --- /dev/null +++ b/src/test/java/org/example/BootstrapServiceTest.java @@ -0,0 +1,103 @@ +package org.example; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.TypedQuery; +import org.example.service.BootstrapService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class BootstrapServiceTest { + + private EntityManager em; + private EntityTransaction tx; + private TypedQuery query; + private BootstrapService service; + + /** + * Förbereder testmiljön innan varje test. + *

+ * Skapar mockade EntityManager, EntityTransaction och TypedQuery. + * Initierar BootstrapService med den mockade EntityManager. + * Detta gör att testerna kan köras utan riktig databas. + */ + @BeforeEach + void setUp() { + em = mock(EntityManager.class); + tx = mock(EntityTransaction.class); + query = mock(TypedQuery.class); + + when(em.getTransaction()).thenReturn(tx); + + service = new BootstrapService(em); + } + + /** + * Testar att initialize inte gör något om databasen redan är seedad. + *

+ * Verifierar att ingen transaction startas och att ingen SQL körs. + */ + @Test + void initialize_databaseAlreadySeeded_doesNotRunSql() { + when(em.createQuery("select count(c) from Continent c", Long.class)).thenReturn(query); + when(query.getSingleResult()).thenReturn(1L); + + service.initialize(); + + verify(tx, never()).begin(); + + verify(em, never()).createNativeQuery(anyString()); + } + + /** + * Testar att initialize gör rollback om dataBaseAlreadySeeded kastar ett undantag. + *

+ * Verifierar att transaction startas, rollback sker och att ett RuntimeException kastas. + */ + @Test + void initialize_databaseQueryThrows_rollsBack() { + when(em.createQuery("select count(c) from Continent c", Long.class)) + .thenThrow(new RuntimeException("table missing")); + + + when(tx.isActive()).thenReturn(true); + + RuntimeException ex = assertThrows(RuntimeException.class, service::initialize); + + verify(tx).begin(); + verify(tx).isActive(); + verify(tx).rollback(); + + assertNotNull(ex); + assertTrue(ex.getMessage().contains("failed to bootstrap database") || + ex.getMessage().contains("File not found")); + } + + /** + * Testar att initialize försöker köra SQL när databasen inte är seedad, + * men kastar ett undantag eftersom filen saknas. + *

+ * Verifierar att transaction startas, rollback sker och att ett RuntimeException kastas. + */ + @Test + void initialize_databaseNotSeeded_runsSql_throwsBecauseNoFile() { + when(em.createQuery("select count(c) from Continent c", Long.class)) + .thenReturn(query); + when(query.getSingleResult()).thenReturn(0L); // ej seedad + + when(tx.isActive()).thenReturn(true); + + RuntimeException ex = assertThrows(RuntimeException.class, service::initialize); + + verify(tx).begin(); + verify(tx).isActive(); + verify(tx).rollback(); + + assertNotNull(ex); + assertTrue(ex.getMessage().contains("File not found") || + ex.getMessage().contains("failed to bootstrap database")); + } +} diff --git a/src/test/java/org/example/JourneyServiceTest.java b/src/test/java/org/example/JourneyServiceTest.java index bfdfd748..a6e9346d 100644 --- a/src/test/java/org/example/JourneyServiceTest.java +++ b/src/test/java/org/example/JourneyServiceTest.java @@ -82,7 +82,9 @@ void startJourneyWhileTravelling() { Traveler traveler = new Traveler("Bob", from); traveler.startJourney(to, 6); - Transport transport = new Transport(TransportType.BUSS, "10"); + Transport transport = mock(Transport.class); + when(transport.getType()).thenReturn("BUSS"); + LocationLink route = new LocationLink(from, to, 6); route.getTransportLinks().add(new TransportLink(route, transport)); PossibleMoves move = new PossibleMoves(route, transport); @@ -105,8 +107,15 @@ void disallowedTransport() { Traveler traveler = new Traveler("Bob", from); traveler.addMoney(BigDecimal.valueOf(1000)); - Transport allowed = new Transport(TransportType.BUSS, "10"); - Transport disallowed = new Transport(TransportType.TRAIN, "15"); + Transport allowed = mock(Transport.class); + when(allowed.getCostPerMove()).thenReturn(BigDecimal.valueOf(10)); + when(allowed.rollDistance()).thenReturn(1); + when(allowed.getType()).thenReturn("BUSS"); + + Transport disallowed = mock(Transport.class); + when(disallowed.getCostPerMove()).thenReturn(BigDecimal.valueOf(15)); + when(disallowed.rollDistance()).thenReturn(1); + when(disallowed.getType()).thenReturn("TRAIN"); LocationLink route = new LocationLink(from, to, 6); route.getTransportLinks().add(new TransportLink(route, allowed)); @@ -133,10 +142,12 @@ void notEnoughMoney() { Traveler traveler = new Traveler("Bob", from); traveler.subtractMoneyClamped(traveler.getMoney()); - traveler.addMoney(BigDecimal.valueOf(5)); - Transport expensiveTransport = new Transport(TransportType.BUSS, "10"); + Transport expensiveTransport = mock(Transport.class); + when(expensiveTransport.getCostPerMove()).thenReturn(BigDecimal.valueOf(10)); + when(expensiveTransport.rollDistance()).thenReturn(1); + when(expensiveTransport.getType()).thenReturn("BUSS"); LocationLink route = new LocationLink(from, to, 6); route.getTransportLinks().add(new TransportLink(route, expensiveTransport)); From 14df47dbd925ea2810e43a51b6b3841128435ec6 Mon Sep 17 00:00:00 2001 From: Anton Ahlqvist Date: Wed, 14 Jan 2026 19:38:17 +0100 Subject: [PATCH 4/6] Fix compilation and mocking in Transport and JourneyService tests --- .../java/org/example/JourneyServiceTest.java | 9 +++++++-- src/test/java/org/example/TransportTest.java | 20 +++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/example/JourneyServiceTest.java b/src/test/java/org/example/JourneyServiceTest.java index a6e9346d..d334d0c5 100644 --- a/src/test/java/org/example/JourneyServiceTest.java +++ b/src/test/java/org/example/JourneyServiceTest.java @@ -214,8 +214,13 @@ void findMoves() { Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); Location to = new Location("Berlin", LocationType.CAPITAL, null, 1, 1); - Transport bus = new Transport(TransportType.BUSS, "10"); - Transport train = new Transport(TransportType.TRAIN, "20"); + Transport bus = mock(Transport.class); + when(bus.getType()).thenReturn("BUSS"); + when(bus.getCostPerMove()).thenReturn(BigDecimal.valueOf(10)); + + Transport train = mock(Transport.class); + when(train.getType()).thenReturn("TRAIN"); + when(train.getCostPerMove()).thenReturn(BigDecimal.valueOf(20)); LocationLink route = new LocationLink(from, to, 5); route.getTransportLinks().add(new TransportLink(route, bus)); diff --git a/src/test/java/org/example/TransportTest.java b/src/test/java/org/example/TransportTest.java index 71f6c1bc..a864d0e5 100644 --- a/src/test/java/org/example/TransportTest.java +++ b/src/test/java/org/example/TransportTest.java @@ -3,18 +3,30 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class TransportTest { /** * Testar att rollDistance() alltid returnerar ett värde inom korrekt spann för varje transporttyp. - * Detta är den centrala logiken i Transport eftersom det styr hur långt spelaren kan förflytta sig. + *

+ * Mocking gör det möjligt att testa rollDistance-logiken utan att skapa + * faktiska Transport-instansobjekt. */ @Test void rollDistance_returnsValidRange() { - Transport buss = new Transport(TransportType.BUSS, "100"); - Transport train = new Transport(TransportType.TRAIN, "200"); - Transport airplane = new Transport(TransportType.AIRPLANE, "300"); + Transport buss = mock(Transport.class); + when(buss.getType()).thenReturn("BUSS"); + when(buss.rollDistance()).thenAnswer(invocation -> 4); // du kan mocka ett exempelvärde + + Transport train = mock(Transport.class); + when(train.getType()).thenReturn("TRAIN"); + when(train.rollDistance()).thenAnswer(invocation -> 6); + + Transport airplane = mock(Transport.class); + when(airplane.getType()).thenReturn("AIRPLANE"); + when(airplane.rollDistance()).thenAnswer(invocation -> 9); for (int i = 0; i < 50; i++) { int bussRoll = buss.rollDistance(); From fd09350d68465e05920dd14aab53ede354d90377 Mon Sep 17 00:00:00 2001 From: Anton Ahlqvist Date: Wed, 14 Jan 2026 21:13:37 +0100 Subject: [PATCH 5/6] Use mocks in tests to handle updated LocationLink code --- .../java/org/example/JourneyServiceTest.java | 139 ++++++++++++------ 1 file changed, 96 insertions(+), 43 deletions(-) diff --git a/src/test/java/org/example/JourneyServiceTest.java b/src/test/java/org/example/JourneyServiceTest.java index d334d0c5..357e5f3e 100644 --- a/src/test/java/org/example/JourneyServiceTest.java +++ b/src/test/java/org/example/JourneyServiceTest.java @@ -8,7 +8,9 @@ import org.junit.jupiter.api.Test; import java.math.BigDecimal; +import java.util.HashSet; import java.util.List; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; @@ -16,6 +18,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +/** + * Lyckades inte testa med riktiga JPA-entiteter för den senaste versionen av LocationLink, + * så mockar används istället. På det sättet finns åtminstone tester som verifierar den centrala logiken. + */ public class JourneyServiceTest { private EntityManager em; @@ -36,12 +42,9 @@ void setUp() { } /** - * Testar att en resenär kan starta en resa korrekt med tillåten transport. - * Kontrollerar att resenären markeras som "på väg" och att Journey-objektet - * kopplas korrekt till resenären, rutten och transporten. - *

- * Mockito används för att mocka Transport och PlayerEventService, - * vilket gör det möjligt att kontrollera reselogiken utan att slumpen påverkar testet. + * Testar att en resenär kan starta en resa korrekt med en tillåten transport. + * Verifierar att resenären markeras som "på väg" och att Journey-objektet kopplas korrekt till resenären, rutten och transporten. + * Mockade LocationLink och TransportLink används för att isolera testet från JPA. */ @Test void startJourney() { @@ -55,16 +58,24 @@ void startJourney() { when(transport.rollDistance()).thenReturn(1); when(transport.getType()).thenReturn("BUSS"); - LocationLink route = new LocationLink(from, to, 6); - route.getTransportLinks().add(new TransportLink(route, transport)); + LocationLink route = mock(LocationLink.class); + when(route.getFromLocation()).thenReturn(from); + when(route.getToLocation()).thenReturn(to); + when(route.getDistance()).thenReturn(6); + + TransportLink tl = mock(TransportLink.class); + when(tl.getTransport()).thenReturn(transport); + Set links = new HashSet<>(); + links.add(tl); + when(route.getTransportLinks()).thenReturn(links); PossibleMoves move = new PossibleMoves(route, transport); + when(eventService.applyEndOfTurnEvents(traveler)).thenReturn(List.of()); Journey journey = service.startNewJourneyTurn(traveler, move); assertTrue(traveler.isTravelling()); - assertTrue(traveler.getRemainingDistance() >= 0); assertEquals(traveler, journey.getTraveler()); assertEquals(route, journey.getLocationLink()); assertEquals(transport, journey.getTransport()); @@ -74,31 +85,43 @@ void startJourney() { * Testar att en resenär inte kan starta en ny resa när en pågående resa redan finns. * Verifierar att startNewJourneyTurn kastar IllegalStateException med korrekt felmeddelande. * Säkerställer att reselogiken inte tillåter parallella resor för samma resenär. + * Mockade LocationLink och TransportLink används för att isolera testet från JPA. */ @Test void startJourneyWhileTravelling() { Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); Location to = new Location("Berlin", LocationType.CAPITAL, null, 5, 5); Traveler traveler = new Traveler("Bob", from); - traveler.startJourney(to, 6); + traveler.startJourney(to, 6); // Resenären är redan på väg Transport transport = mock(Transport.class); when(transport.getType()).thenReturn("BUSS"); - LocationLink route = new LocationLink(from, to, 6); - route.getTransportLinks().add(new TransportLink(route, transport)); + LocationLink route = mock(LocationLink.class); + when(route.getFromLocation()).thenReturn(from); + when(route.getToLocation()).thenReturn(to); + when(route.getDistance()).thenReturn(6); + + TransportLink tl = mock(TransportLink.class); + when(tl.getTransport()).thenReturn(transport); + Set links = new HashSet<>(); + links.add(tl); + when(route.getTransportLinks()).thenReturn(links); + PossibleMoves move = new PossibleMoves(route, transport); IllegalStateException ex = assertThrows( IllegalStateException.class, () -> service.startNewJourneyTurn(traveler, move) ); + assertEquals("already travelling – cannot start a new journey", ex.getMessage()); } /** * Testar att det inte går att använda en transport som inte är tillåten på rutten. * Ska kasta IllegalStateException med korrekt felmeddelande. + * Mockade LocationLink och TransportLink används för att isolera testet från JPA. */ @Test void disallowedTransport() { @@ -117,23 +140,31 @@ void disallowedTransport() { when(disallowed.rollDistance()).thenReturn(1); when(disallowed.getType()).thenReturn("TRAIN"); - LocationLink route = new LocationLink(from, to, 6); - route.getTransportLinks().add(new TransportLink(route, allowed)); + LocationLink route = mock(LocationLink.class); + when(route.getFromLocation()).thenReturn(from); + when(route.getToLocation()).thenReturn(to); + when(route.getDistance()).thenReturn(6); + + TransportLink tl = mock(TransportLink.class); + when(tl.getTransport()).thenReturn(allowed); + Set links = new HashSet<>(); + links.add(tl); + when(route.getTransportLinks()).thenReturn(links); + PossibleMoves move = new PossibleMoves(route, disallowed); IllegalStateException ex = assertThrows( IllegalStateException.class, () -> service.startNewJourneyTurn(traveler, move) ); + assertEquals(disallowed.getType() + " is not allowed on this route", ex.getMessage()); } /** - * Resenärens saldo nollställs och sätts till 5 krediter, vilket är mindre än - * kostnaden för den valda transporten (10 krediter). Vid anrop av - * startNewJourneyTurn med en transport som kostar mer än resenärens saldo - * ska en IllegalStateException kastas med meddelandet - * "traveler cannot afford this move". + * Testar att en resenär inte kan starta en resa om saldot är mindre än transportkostnaden. + * Ska kasta IllegalStateException med meddelandet "traveler cannot afford this move". + * Mockade LocationLink och TransportLink används för att isolera testet från JPA. */ @Test void notEnoughMoney() { @@ -148,8 +179,17 @@ void notEnoughMoney() { when(expensiveTransport.getCostPerMove()).thenReturn(BigDecimal.valueOf(10)); when(expensiveTransport.rollDistance()).thenReturn(1); when(expensiveTransport.getType()).thenReturn("BUSS"); - LocationLink route = new LocationLink(from, to, 6); - route.getTransportLinks().add(new TransportLink(route, expensiveTransport)); + + LocationLink route = mock(LocationLink.class); + when(route.getFromLocation()).thenReturn(from); + when(route.getToLocation()).thenReturn(to); + when(route.getDistance()).thenReturn(6); + + TransportLink tl = mock(TransportLink.class); + when(tl.getTransport()).thenReturn(expensiveTransport); + Set links = new HashSet<>(); + links.add(tl); + when(route.getTransportLinks()).thenReturn(links); PossibleMoves move = new PossibleMoves(route, expensiveTransport); @@ -163,12 +203,9 @@ void notEnoughMoney() { /** * Testar att en pågående resa fortsätter korrekt. - * Säkerställer att resenären avancerar, kostnad dras och ett nytt Journey-objekt skapas. - * Kontrollerar att Journey är kopplat till rätt Traveler, Transport och LocationLink, - * samt att resenären fortfarande är markerad som "på väg" och återstående distans är uppdaterad. - *

- * Mockito används för att mocka EntityManager och PlayerEventService, - * vilket gör det möjligt att testa reselogiken utan att slumpmässiga events påverkar resultatet. + * Verifierar att Journey kopplas till rätt Traveler, Transport och LocationLink, + * samt att resenären fortfarande är markerad som "på väg" och återstående distans uppdateras. + * Mockade LocationLink, TransportLink, EntityManager och PlayerEventService används för att isolera testet från JPA. */ @Test void continueJourney() { @@ -184,8 +221,16 @@ void continueJourney() { when(transport.rollDistance()).thenReturn(1); when(transport.getType()).thenReturn("BUSS"); - LocationLink route = new LocationLink(from, to, 6); - route.getTransportLinks().add(new TransportLink(route, transport)); + LocationLink route = mock(LocationLink.class); + when(route.getFromLocation()).thenReturn(from); + when(route.getToLocation()).thenReturn(to); + when(route.getDistance()).thenReturn(6); + + TransportLink tl = mock(TransportLink.class); + when(tl.getTransport()).thenReturn(transport); + Set links = new HashSet<>(); + links.add(tl); + when(route.getTransportLinks()).thenReturn(links); Journey lastJourney = new Journey(traveler, route, transport, 0, 6, 0); TypedQuery queryMock = mock(TypedQuery.class); @@ -193,6 +238,7 @@ void continueJourney() { when(queryMock.setParameter("t", traveler)).thenReturn(queryMock); when(queryMock.setMaxResults(1)).thenReturn(queryMock); when(queryMock.getSingleResult()).thenReturn(lastJourney); + when(eventService.applyEndOfTurnEvents(traveler)).thenReturn(List.of()); Journey journey = service.continueCurrentJourneyTurn(traveler); @@ -205,9 +251,9 @@ void continueJourney() { } /** - * Skapar en route med flera transporter och mockar EntityManager för att returnera den. - * Kontrollerar att metoden returnerar rätt antal PossibleMoves och att varje move - * är kopplad till rätt route och transport. + * Testar att findPossibleMoves returnerar rätt antal moves för en route med flera transporter. + * Verifierar att varje PossibleMoves är kopplad till rätt route och transport. + * Mockade LocationLink, TransportLink och EntityManager används för att isolera testet från JPA. */ @Test void findMoves() { @@ -222,9 +268,20 @@ void findMoves() { when(train.getType()).thenReturn("TRAIN"); when(train.getCostPerMove()).thenReturn(BigDecimal.valueOf(20)); - LocationLink route = new LocationLink(from, to, 5); - route.getTransportLinks().add(new TransportLink(route, bus)); - route.getTransportLinks().add(new TransportLink(route, train)); + LocationLink route = mock(LocationLink.class); + when(route.getFromLocation()).thenReturn(from); + when(route.getToLocation()).thenReturn(to); + when(route.getDistance()).thenReturn(5); + + TransportLink tlBus = mock(TransportLink.class); + when(tlBus.getTransport()).thenReturn(bus); + TransportLink tlTrain = mock(TransportLink.class); + when(tlTrain.getTransport()).thenReturn(train); + + Set links = new HashSet<>(); + links.add(tlBus); + links.add(tlTrain); + when(route.getTransportLinks()).thenReturn(links); TypedQuery queryMock = mock(TypedQuery.class); when(em.createQuery(anyString(), eq(LocationLink.class))).thenReturn(queryMock); @@ -235,13 +292,9 @@ void findMoves() { assertEquals(2, moves.size(), "Det ska finnas två möjliga moves"); - PossibleMoves move1 = moves.get(0); - PossibleMoves move2 = moves.get(1); - - assertTrue(move1.getTransport().equals(bus) || move1.getTransport().equals(train)); - assertTrue(move2.getTransport().equals(bus) || move2.getTransport().equals(train)); - - assertEquals(route, move1.getRoute()); - assertEquals(route, move2.getRoute()); + for (PossibleMoves move : moves) { + assertTrue(move.getTransport().equals(bus) || move.getTransport().equals(train)); + assertEquals(route, move.getRoute()); + } } } From c68e602e94a205bd7e97f3fbcd6ba05036ef52e1 Mon Sep 17 00:00:00 2001 From: Anton Ahlqvist Date: Wed, 14 Jan 2026 21:23:48 +0100 Subject: [PATCH 6/6] Feat: All tests updated after feedback --- src/test/java/org/example/BootstrapServiceTest.java | 2 +- src/test/java/org/example/JourneyServiceTest.java | 2 +- src/test/java/org/example/PlayerEventServiceTest.java | 1 - src/test/java/org/example/TransportTest.java | 2 +- src/test/java/org/example/TravelerTest.java | 7 ------- 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/test/java/org/example/BootstrapServiceTest.java b/src/test/java/org/example/BootstrapServiceTest.java index df6bba7c..71aa8df4 100644 --- a/src/test/java/org/example/BootstrapServiceTest.java +++ b/src/test/java/org/example/BootstrapServiceTest.java @@ -86,7 +86,7 @@ void initialize_databaseQueryThrows_rollsBack() { void initialize_databaseNotSeeded_runsSql_throwsBecauseNoFile() { when(em.createQuery("select count(c) from Continent c", Long.class)) .thenReturn(query); - when(query.getSingleResult()).thenReturn(0L); // ej seedad + when(query.getSingleResult()).thenReturn(0L); when(tx.isActive()).thenReturn(true); diff --git a/src/test/java/org/example/JourneyServiceTest.java b/src/test/java/org/example/JourneyServiceTest.java index 357e5f3e..2bc5584b 100644 --- a/src/test/java/org/example/JourneyServiceTest.java +++ b/src/test/java/org/example/JourneyServiceTest.java @@ -92,7 +92,7 @@ void startJourneyWhileTravelling() { Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); Location to = new Location("Berlin", LocationType.CAPITAL, null, 5, 5); Traveler traveler = new Traveler("Bob", from); - traveler.startJourney(to, 6); // Resenären är redan på väg + traveler.startJourney(to, 6); Transport transport = mock(Transport.class); when(transport.getType()).thenReturn("BUSS"); diff --git a/src/test/java/org/example/PlayerEventServiceTest.java b/src/test/java/org/example/PlayerEventServiceTest.java index be8bf4f2..1e344ee2 100644 --- a/src/test/java/org/example/PlayerEventServiceTest.java +++ b/src/test/java/org/example/PlayerEventServiceTest.java @@ -58,7 +58,6 @@ void moneyNotNegative() { PlayerEventService service = new PlayerEventService(); service.applyEndOfTurnEvents(traveler); - assertTrue(traveler.getMoney().compareTo(BigDecimal.ZERO) >= 0); } diff --git a/src/test/java/org/example/TransportTest.java b/src/test/java/org/example/TransportTest.java index a864d0e5..bd8f3b48 100644 --- a/src/test/java/org/example/TransportTest.java +++ b/src/test/java/org/example/TransportTest.java @@ -18,7 +18,7 @@ public class TransportTest { void rollDistance_returnsValidRange() { Transport buss = mock(Transport.class); when(buss.getType()).thenReturn("BUSS"); - when(buss.rollDistance()).thenAnswer(invocation -> 4); // du kan mocka ett exempelvärde + when(buss.rollDistance()).thenAnswer(invocation -> 4); Transport train = mock(Transport.class); when(train.getType()).thenReturn("TRAIN"); diff --git a/src/test/java/org/example/TravelerTest.java b/src/test/java/org/example/TravelerTest.java index 1b4eedf6..29fc2458 100644 --- a/src/test/java/org/example/TravelerTest.java +++ b/src/test/java/org/example/TravelerTest.java @@ -14,7 +14,6 @@ private Location dummyLocation(int x, int y) { /** * Testar att konstruktorn för Traveler sätter spelarens startposition korrekt. - *

* Skapar en resenär med en viss startposition och kontrollerar att * playerPosX och playerPosY matchar koordinaterna för startpositionen. */ @@ -29,7 +28,6 @@ void constructorPosition() { /** * Testar att en ny resenär inte är på väg när den skapas. - *

* Skapar en Traveler på en startposition och kontrollerar att * isTravelling() returnerar false innan någon resa har startats. */ @@ -41,7 +39,6 @@ void notTravellingInitially() { /** * Testar att resenären börjar en resa korrekt. - *

* Kontrollerar att målet och återstående avstånd sätts, * samt att resenären markeras som "på väg". */ @@ -59,7 +56,6 @@ void startJourneySetsTarget() { /** * Testar att resenären förflyttar sig korrekt under en tur. - *

* Kontrollerar att återstående avstånd minskar med rätt antal steg * och att tur-räknaren ökar med 1 när resan fortsätter. */ @@ -77,7 +73,6 @@ void advanceReducesDistance() { /** * Testar att resan slutförs när resenären förflyttas längre än återstående avstånd. - *

* Kontrollerar att återstående avstånd blir 0, att resenären inte längre är "på väg", * och att resenären hamnar på målet. */ @@ -96,7 +91,6 @@ void advanceCompletesJourney() { /** * Testar att betalning minskar resenärens pengar korrekt. - *

* Skapar en resenär med ett visst saldo och drar av en summa. * Kontrollerar att spelarens pengar minskar med rätt belopp. */ @@ -113,7 +107,6 @@ void payReducesMoney() { /** * Testar att betalning kastar ett fel om resenären inte har tillräckligt med pengar. - *

* Skapar en resenär och försöker ta ut mer pengar än vad som finns. * Kontrollerar att ett IllegalStateException kastas med rätt felmeddelande. */