diff --git a/src/test/java/org/example/BootstrapServiceTest.java b/src/test/java/org/example/BootstrapServiceTest.java new file mode 100644 index 00000000..71aa8df4 --- /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); + + 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 new file mode 100644 index 00000000..2bc5584b --- /dev/null +++ b/src/test/java/org/example/JourneyServiceTest.java @@ -0,0 +1,300 @@ +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.HashSet; +import java.util.List; +import java.util.Set; + +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; + +/** + * 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; + 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 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() { + 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 = 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()); + 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. + * 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); + + Transport transport = mock(Transport.class); + when(transport.getType()).thenReturn("BUSS"); + + 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() { + 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 = 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 = 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()); + } + + /** + * 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() { + 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 = mock(Transport.class); + when(expensiveTransport.getCostPerMove()).thenReturn(BigDecimal.valueOf(10)); + when(expensiveTransport.rollDistance()).thenReturn(1); + when(expensiveTransport.getType()).thenReturn("BUSS"); + + 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); + + 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. + * 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() { + 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 = 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); + 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); + } + + /** + * 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() { + Location from = new Location("Stockholm", LocationType.CAPITAL, null, 0, 0); + Location to = new Location("Berlin", LocationType.CAPITAL, null, 1, 1); + + 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 = 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); + 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"); + + for (PossibleMoves move : moves) { + assertTrue(move.getTransport().equals(bus) || move.getTransport().equals(train)); + assertEquals(route, move.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..1e344ee2 --- /dev/null +++ b/src/test/java/org/example/PlayerEventServiceTest.java @@ -0,0 +1,98 @@ +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()); + } + } +} diff --git a/src/test/java/org/example/TransportTest.java b/src/test/java/org/example/TransportTest.java new file mode 100644 index 00000000..bd8f3b48 --- /dev/null +++ b/src/test/java/org/example/TransportTest.java @@ -0,0 +1,41 @@ +package org.example; + +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. + *

+ * Mocking gör det möjligt att testa rollDistance-logiken utan att skapa + * faktiska Transport-instansobjekt. + */ + @Test + void rollDistance_returnsValidRange() { + Transport buss = mock(Transport.class); + when(buss.getType()).thenReturn("BUSS"); + when(buss.rollDistance()).thenAnswer(invocation -> 4); + + 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(); + 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..29fc2458 --- /dev/null +++ b/src/test/java/org/example/TravelerTest.java @@ -0,0 +1,121 @@ +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()); + } +}