diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..fa71f4fd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'temurin' + cache: maven + + - name: Compile + run: mvn -B clean compile + + - name: Test + run: mvn -B test diff --git a/.gitignore b/.gitignore index b7d7ac33..d83e7f88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ /target /local +dependency-reduced-pom.xml # Eclipse, Netbeans and IntelliJ files /.* !.gitignore +!.github /nbproject /*.ipr /*.iws diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md new file mode 100644 index 00000000..92c6ebe2 --- /dev/null +++ b/MIGRATION_NOTES.md @@ -0,0 +1,58 @@ +# Migration Notes: Java 9 → Java 11 + +## Java Version Change + +- **From**: Java 9 (source/target 1.9) +- **To**: Java 11 (using `11`) +- The `` flag replaces the legacy ``/`` configuration and provides stronger cross-compilation guarantees. + +## Plugin Upgrades + +| Plugin | Old Version | New Version | Notes | +|--------|-------------|-------------|-------| +| maven-compiler-plugin | 3.1 | 3.11.0 | Switched from source/target to release flag | +| maven-surefire-plugin | (none) | 3.2.5 | Added for test execution | +| maven-failsafe-plugin | (none) | 3.2.5 | Added for integration test support | +| maven-enforcer-plugin | (none) | 3.5.0 | Added to enforce Java 11+ at build time | +| maven-javadoc-plugin | (none) | 3.6.3 | Added with `none` (Java 11 doclint is stricter) | +| maven-shade-plugin | (unversioned) | 3.6.2 | Pinned explicit version (was unversioned, which Maven warns is unstable) | + +Build encoding is pinned to UTF-8 for both source (`project.build.sourceEncoding`) and reporting (`project.reporting.outputEncoding`). + +## Dependency Upgrades + +| Dependency | Old Version | New Version | Notes | +|------------|-------------|-------------|-------| +| JUnit | 4.11 | 4.13.2 | Latest JUnit 4.x; added `test` | +| JMH (jmh-core) | 1.17.4 | 1.37 | Java 11 compatible | +| JMH (jmh-generator-annprocess) | 1.17.4 | 1.37 | Java 11 compatible | + +## Code Fixes Required + +Minimal changes were needed: + +- **Moved `ReadPositiveIntParam.java`** from `src/main/java` to `src/test/java` — this file contained JUnit `@Test` annotations but was incorrectly placed in the main source tree. With JUnit correctly scoped to `test`, it must reside under `src/test/java`. +- No Nashorn JavaScript engine references found +- No internal `sun.*` or `com.sun.*` API usage +- No Java EE modules (removed in Java 11) were used +- Deprecation warning in `Util.java` (deprecated API usage) — informational only, no breaking change + +## New Test Coverage + +Added integration tests covering core chapters: + +| Test File | Chapter | Coverage | +|-----------|---------|----------| +| `FilteringApplesTest.java` | Chapter 1 | Lambda expressions, method references, Predicate-based filtering | +| `StreamBasicsTest.java` | Chapter 4 | Stream creation, filtering, sorting, Java 7 vs Java 8 comparison | +| `StreamProcessingTest.java` | Chapter 5 | Trader/Transaction queries, reduce, distinct, anyMatch | +| `OptionalTest.java` | Chapter 10 | Optional creation, map, flatMap, orElse, filter, stream() | +| `CompletableFutureTest.java` | Chapter 11 | supplyAsync, thenApply, thenCombine, thenCompose, exceptionally | +| `DateTimeTest.java` | Chapter 12 | LocalDate, LocalTime, LocalDateTime, Duration, TemporalAdjusters, DateTimeFormatter | + +## CI Workflow Added + +- GitHub Actions workflow (`.github/workflows/ci.yml`) +- Triggers on push/PR to master/main branches +- Uses Temurin JDK 11 with Maven caching +- Runs `mvn -B clean compile` and `mvn -B test` diff --git a/README.md b/README.md index 9ee848d5..61360259 100644 --- a/README.md +++ b/README.md @@ -29,31 +29,34 @@ The source code for all examples can be found in the directory [src/main/java/la * Appendix D: Lambdas and JVM bytecode We will update the repository as we update the book. Stay tuned! -### Make sure to have JDK8 installed -The latest binary can be found here: http://www.oracle.com/technetwork/java/javase/overview/java8-2100321.html +### Prerequisites -$ java -version - -java version "1.8.0_05" -Java(TM) SE Runtime Environment (build 1.8.0_05-b13) -Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode) +**JDK 11** or higher is required. You can use any distribution (Temurin, OpenJDK, Oracle JDK, etc.). +``` +$ java -version +openjdk version "11.0.x" ... +``` -You can download a preview version here: https://jdk8.java.net/ +Download Temurin JDK 11: https://adoptium.net/temurin/releases/?version=11 ### Compile/Run the examples -Using maven: -$ mvn compile +Using Maven: +``` +$ mvn clean compile $ cd target/classes - $ java lambdasinaction/chap1/FilteringApples +``` +Run tests: -Alternatively you can compile the files manually inside the directory src/main/java +``` +$ mvn test +``` -You can also import the project in your favorite IDE: - * In IntelliJ use "File->Open" menu and navigate to the folder where the project resides - * In Eclipse use "File->Import->Existing Maven Projects" (also modify "Reduntant super interfaces" to report as Warnings instead of Errors - * In Netbeans use "File->Open Project" menu \ No newline at end of file +Alternatively you can import the project in your favorite IDE: + * In IntelliJ use "File->Open" menu and navigate to the folder where the project resides + * In Eclipse use "File->Import->Existing Maven Projects" + * In Netbeans use "File->Open Project" menu \ No newline at end of file diff --git a/pom.xml b/pom.xml index 10e5035e..3ff0badc 100644 --- a/pom.xml +++ b/pom.xml @@ -10,23 +10,26 @@ UTF-8 + UTF-8 + 11 org.openjdk.jmh jmh-core - 1.17.4 + 1.37 org.openjdk.jmh jmh-generator-annprocess - 1.17.4 + 1.37 junit junit - 4.11 + 4.13.2 + test @@ -35,15 +38,54 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.11.0 - 1.9 - 1.9 + 11 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + enforce-java + + enforce + + + + + [11,) + Java 11 or higher is required to build this project. + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + none org.apache.maven.plugins maven-shade-plugin + 3.6.2 package @@ -63,4 +105,4 @@ - \ No newline at end of file + diff --git a/src/test/java/lambdasinaction/chap1/FilteringApplesTest.java b/src/test/java/lambdasinaction/chap1/FilteringApplesTest.java new file mode 100644 index 00000000..752fb2df --- /dev/null +++ b/src/test/java/lambdasinaction/chap1/FilteringApplesTest.java @@ -0,0 +1,64 @@ +package lambdasinaction.chap1; + +import lambdasinaction.chap1.FilteringApples.Apple; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +public class FilteringApplesTest { + + private final List inventory = Arrays.asList( + new Apple(80, "green"), + new Apple(155, "green"), + new Apple(120, "red")); + + @Test + public void filterGreenApples() { + List result = FilteringApples.filterGreenApples(inventory); + assertEquals(2, result.size()); + assertTrue(result.stream().allMatch(a -> "green".equals(a.getColor()))); + } + + @Test + public void filterHeavyApples() { + List result = FilteringApples.filterHeavyApples(inventory); + assertEquals(1, result.size()); + assertTrue(result.get(0).getWeight() > 150); + } + + @Test + public void filterWithMethodReference() { + List greens = FilteringApples.filterApples(inventory, FilteringApples::isGreenApple); + assertEquals(2, greens.size()); + + List heavies = FilteringApples.filterApples(inventory, FilteringApples::isHeavyApple); + assertEquals(1, heavies.size()); + } + + @Test + public void filterWithLambda() { + List greens = FilteringApples.filterApples(inventory, a -> "green".equals(a.getColor())); + assertEquals(2, greens.size()); + + List heavies = FilteringApples.filterApples(inventory, a -> a.getWeight() > 150); + assertEquals(1, heavies.size()); + } + + @Test + public void filterWithComplexPredicate() { + List result = FilteringApples.filterApples(inventory, + a -> a.getWeight() < 80 || "brown".equals(a.getColor())); + assertTrue(result.isEmpty()); + } + + @Test + public void appleToString() { + Apple apple = new Apple(80, "green"); + String str = apple.toString(); + assertTrue(str.contains("green")); + assertTrue(str.contains("80")); + } +} diff --git a/src/test/java/lambdasinaction/chap10/OptionalTest.java b/src/test/java/lambdasinaction/chap10/OptionalTest.java new file mode 100644 index 00000000..790db21c --- /dev/null +++ b/src/test/java/lambdasinaction/chap10/OptionalTest.java @@ -0,0 +1,84 @@ +package lambdasinaction.chap10; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; + +import static org.junit.Assert.*; + +public class OptionalTest { + + @Test + public void getCarInsuranceNameWithEmptyPerson() { + OptionalMain main = new OptionalMain(); + String result = main.getCarInsuranceName(Optional.empty()); + assertEquals("Unknown", result); + } + + @Test + public void optionalOfNullable() { + Optional opt = Optional.ofNullable(null); + assertFalse(opt.isPresent()); + + Optional present = Optional.ofNullable("hello"); + assertTrue(present.isPresent()); + assertEquals("hello", present.get()); + } + + @Test + public void optionalMap() { + Optional name = Optional.of("Java"); + Optional len = name.map(String::length); + assertEquals(Integer.valueOf(4), len.get()); + } + + @Test + public void optionalFlatMap() { + Optional> nested = Optional.of(Optional.of("nested")); + Optional flat = nested.flatMap(x -> x); + assertEquals("nested", flat.get()); + } + + @Test + public void optionalOrElse() { + Optional empty = Optional.empty(); + assertEquals("default", empty.orElse("default")); + + Optional present = Optional.of("value"); + assertEquals("value", present.orElse("default")); + } + + @Test + public void optionalFilter() { + Optional opt = Optional.of("hello"); + assertTrue(opt.filter(s -> s.startsWith("h")).isPresent()); + assertFalse(opt.filter(s -> s.startsWith("z")).isPresent()); + } + + @Test + public void optionalIfPresent() { + Optional opt = Optional.of("test"); + StringBuilder sb = new StringBuilder(); + opt.ifPresent(sb::append); + assertEquals("test", sb.toString()); + } + + @Test + public void optionalStream() { + Optional present = Optional.of("value"); + long count = present.stream().count(); + assertEquals(1, count); + + Optional empty = Optional.empty(); + long emptyCount = empty.stream().count(); + assertEquals(0, emptyCount); + } + + @Test + public void insuranceNameAccess() { + Insurance insurance = new Insurance(); + assertNull(insurance.getName()); + } +} diff --git a/src/main/java/lambdasinaction/chap10/ReadPositiveIntParam.java b/src/test/java/lambdasinaction/chap10/ReadPositiveIntParam.java similarity index 100% rename from src/main/java/lambdasinaction/chap10/ReadPositiveIntParam.java rename to src/test/java/lambdasinaction/chap10/ReadPositiveIntParam.java diff --git a/src/test/java/lambdasinaction/chap11/CompletableFutureTest.java b/src/test/java/lambdasinaction/chap11/CompletableFutureTest.java new file mode 100644 index 00000000..3b8e72e2 --- /dev/null +++ b/src/test/java/lambdasinaction/chap11/CompletableFutureTest.java @@ -0,0 +1,91 @@ +package lambdasinaction.chap11; + +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.*; + +public class CompletableFutureTest { + + @Test + public void supplyAsync() throws ExecutionException, InterruptedException { + CompletableFuture future = CompletableFuture.supplyAsync(() -> "hello"); + assertEquals("hello", future.get()); + } + + @Test + public void thenApply() throws ExecutionException, InterruptedException { + CompletableFuture future = CompletableFuture + .supplyAsync(() -> "hello") + .thenApply(String::length); + assertEquals(Integer.valueOf(5), future.get()); + } + + @Test + public void thenCombine() throws ExecutionException, InterruptedException { + CompletableFuture future = CompletableFuture + .supplyAsync(() -> "hello") + .thenCombine( + CompletableFuture.supplyAsync(() -> " world"), + String::concat); + assertEquals("hello world", future.get()); + } + + @Test + public void thenCompose() throws ExecutionException, InterruptedException { + CompletableFuture future = CompletableFuture + .supplyAsync(() -> "hello") + .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " world")); + assertEquals("hello world", future.get()); + } + + @Test + public void exceptionally() throws ExecutionException, InterruptedException { + CompletableFuture future = CompletableFuture + .supplyAsync(() -> { throw new RuntimeException("error"); }) + .exceptionally(ex -> "recovered"); + assertEquals("recovered", future.get()); + } + + @Test + public void shopGetPriceFormat() { + Shop shop = new Shop("BestShop"); + String price = shop.getPrice("iPhone"); + assertNotNull(price); + String[] parts = price.split(":"); + assertEquals(3, parts.length); + assertEquals("BestShop", parts[0]); + Double.parseDouble(parts[1]); + Discount.Code.valueOf(parts[2]); + } + + @Test + public void quoteParse() { + Quote quote = Quote.parse("MyShop:100.0:GOLD"); + assertEquals("MyShop", quote.getShopName()); + assertEquals(100.0, quote.getPrice(), 0.01); + assertEquals(Discount.Code.GOLD, quote.getDiscountCode()); + } + + @Test + public void completableFutureAllOf() throws ExecutionException, InterruptedException { + CompletableFuture f1 = CompletableFuture.supplyAsync(() -> "a"); + CompletableFuture f2 = CompletableFuture.supplyAsync(() -> "b"); + CompletableFuture all = CompletableFuture.allOf(f1, f2); + all.get(); + assertEquals("a", f1.get()); + assertEquals("b", f2.get()); + } + + @Test + public void completableFutureAnyOf() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture f1 = CompletableFuture.supplyAsync(() -> "first"); + CompletableFuture f2 = CompletableFuture.supplyAsync(() -> "second"); + Object result = CompletableFuture.anyOf(f1, f2).get(5, TimeUnit.SECONDS); + assertNotNull(result); + } +} diff --git a/src/test/java/lambdasinaction/chap12/DateTimeTest.java b/src/test/java/lambdasinaction/chap12/DateTimeTest.java new file mode 100644 index 00000000..366f1af4 --- /dev/null +++ b/src/test/java/lambdasinaction/chap12/DateTimeTest.java @@ -0,0 +1,119 @@ +package lambdasinaction.chap12; + +import org.junit.Test; + +import java.time.*; +import java.time.chrono.JapaneseDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; + +import static java.time.temporal.TemporalAdjusters.lastDayOfMonth; +import static java.time.temporal.TemporalAdjusters.nextOrSame; +import static org.junit.Assert.*; + +public class DateTimeTest { + + @Test + public void localDateCreation() { + LocalDate date = LocalDate.of(2014, 3, 18); + assertEquals(2014, date.getYear()); + assertEquals(Month.MARCH, date.getMonth()); + assertEquals(18, date.getDayOfMonth()); + assertEquals(DayOfWeek.TUESDAY, date.getDayOfWeek()); + assertEquals(31, date.lengthOfMonth()); + assertFalse(date.isLeapYear()); + } + + @Test + public void localDateChronoFieldAccess() { + LocalDate date = LocalDate.of(2014, 3, 18); + assertEquals(2014, date.get(ChronoField.YEAR)); + assertEquals(3, date.get(ChronoField.MONTH_OF_YEAR)); + assertEquals(18, date.get(ChronoField.DAY_OF_MONTH)); + } + + @Test + public void localTimeCreation() { + LocalTime time = LocalTime.of(13, 45, 20); + assertEquals(13, time.getHour()); + assertEquals(45, time.getMinute()); + assertEquals(20, time.getSecond()); + } + + @Test + public void localDateTimeCreation() { + LocalDate date = LocalDate.of(2014, 3, 18); + LocalTime time = LocalTime.of(13, 45, 20); + LocalDateTime dt = LocalDateTime.of(date, time); + assertEquals(date, dt.toLocalDate()); + assertEquals(time, dt.toLocalTime()); + } + + @Test + public void duration() { + LocalTime t1 = LocalTime.of(13, 45, 10); + LocalTime t2 = LocalTime.of(13, 45, 20); + Duration d = Duration.between(t1, t2); + assertEquals(10, d.getSeconds()); + } + + @Test + public void durationOfChronoUnit() { + Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES); + assertEquals(180, threeMinutes.getSeconds()); + } + + @Test + public void temporalAdjusterNextOrSame() { + LocalDate date = LocalDate.of(2014, 3, 18); + LocalDate sunday = date.with(nextOrSame(DayOfWeek.SUNDAY)); + assertEquals(DayOfWeek.SUNDAY, sunday.getDayOfWeek()); + assertTrue(sunday.isAfter(date) || sunday.isEqual(date)); + } + + @Test + public void temporalAdjusterLastDayOfMonth() { + LocalDate date = LocalDate.of(2014, 3, 18); + LocalDate lastDay = date.with(lastDayOfMonth()); + assertEquals(31, lastDay.getDayOfMonth()); + } + + @Test + public void dateTimeFormatter() { + LocalDate date = LocalDate.of(2014, 3, 18); + String isoFormatted = date.format(DateTimeFormatter.ISO_LOCAL_DATE); + assertEquals("2014-03-18", isoFormatted); + + DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + assertEquals("18/03/2014", date.format(custom)); + } + + @Test + public void parseDateFromString() { + LocalDate parsed = LocalDate.parse("2014-03-18"); + assertEquals(LocalDate.of(2014, 3, 18), parsed); + } + + @Test + public void instantCreation() { + Instant instant = Instant.ofEpochSecond(0); + assertEquals(0, instant.getEpochSecond()); + } + + @Test + public void japaneseDateConversion() { + LocalDate date = LocalDate.of(2014, 3, 18); + JapaneseDate japaneseDate = JapaneseDate.from(date); + assertNotNull(japaneseDate); + } + + @Test + public void periodBetweenDates() { + LocalDate d1 = LocalDate.of(2014, 1, 1); + LocalDate d2 = LocalDate.of(2014, 3, 18); + Period period = Period.between(d1, d2); + assertEquals(2, period.getMonths()); + assertEquals(17, period.getDays()); + } +} diff --git a/src/test/java/lambdasinaction/chap4/StreamBasicsTest.java b/src/test/java/lambdasinaction/chap4/StreamBasicsTest.java new file mode 100644 index 00000000..f6aa6e65 --- /dev/null +++ b/src/test/java/lambdasinaction/chap4/StreamBasicsTest.java @@ -0,0 +1,59 @@ +package lambdasinaction.chap4; + +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class StreamBasicsTest { + + @Test + public void dishMenuIsNotEmpty() { + assertFalse(Dish.menu.isEmpty()); + assertEquals(9, Dish.menu.size()); + } + + @Test + public void lowCaloricDishesJava7() { + List names = StreamBasic.getLowCaloricDishesNamesInJava7(Dish.menu); + assertFalse(names.isEmpty()); + assertTrue(names.stream().allMatch(name -> + Dish.menu.stream() + .filter(d -> d.getName().equals(name)) + .findFirst() + .map(d -> d.getCalories() < 400) + .orElse(false))); + } + + @Test + public void lowCaloricDishesJava8() { + List names = StreamBasic.getLowCaloricDishesNamesInJava8(Dish.menu); + assertFalse(names.isEmpty()); + assertTrue(names.stream().allMatch(name -> + Dish.menu.stream() + .filter(d -> d.getName().equals(name)) + .findFirst() + .map(d -> d.getCalories() < 400) + .orElse(false))); + } + + @Test + public void java7AndJava8ProduceSameResults() { + List java7 = StreamBasic.getLowCaloricDishesNamesInJava7(Dish.menu); + List java8 = StreamBasic.getLowCaloricDishesNamesInJava8(Dish.menu); + assertEquals(java7, java8); + } + + @Test + public void dishTypes() { + assertTrue(Dish.menu.stream().anyMatch(d -> d.getType() == Dish.Type.MEAT)); + assertTrue(Dish.menu.stream().anyMatch(d -> d.getType() == Dish.Type.FISH)); + assertTrue(Dish.menu.stream().anyMatch(d -> d.getType() == Dish.Type.OTHER)); + } + + @Test + public void vegetarianDishesExist() { + assertTrue(Dish.menu.stream().anyMatch(Dish::isVegetarian)); + } +} diff --git a/src/test/java/lambdasinaction/chap5/StreamProcessingTest.java b/src/test/java/lambdasinaction/chap5/StreamProcessingTest.java new file mode 100644 index 00000000..49bdf07d --- /dev/null +++ b/src/test/java/lambdasinaction/chap5/StreamProcessingTest.java @@ -0,0 +1,115 @@ +package lambdasinaction.chap5; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.*; + +public class StreamProcessingTest { + + private List transactions; + + @Before + public void setUp() { + Trader raoul = new Trader("Raoul", "Cambridge"); + Trader mario = new Trader("Mario", "Milan"); + Trader alan = new Trader("Alan", "Cambridge"); + Trader brian = new Trader("Brian", "Cambridge"); + + transactions = Arrays.asList( + new Transaction(brian, 2011, 300), + new Transaction(raoul, 2012, 1000), + new Transaction(raoul, 2011, 400), + new Transaction(mario, 2012, 710), + new Transaction(mario, 2012, 700), + new Transaction(alan, 2012, 950)); + } + + @Test + public void findTransactionsIn2011SortedByValue() { + List result = transactions.stream() + .filter(t -> t.getYear() == 2011) + .sorted(comparing(Transaction::getValue)) + .collect(toList()); + + assertEquals(2, result.size()); + assertEquals(300, result.get(0).getValue()); + assertEquals(400, result.get(1).getValue()); + } + + @Test + public void findUniqueCities() { + List cities = transactions.stream() + .map(t -> t.getTrader().getCity()) + .distinct() + .collect(toList()); + + assertEquals(2, cities.size()); + assertTrue(cities.contains("Cambridge")); + assertTrue(cities.contains("Milan")); + } + + @Test + public void findCambridgeTradersSortedByName() { + List traders = transactions.stream() + .map(Transaction::getTrader) + .filter(t -> t.getCity().equals("Cambridge")) + .distinct() + .sorted(comparing(Trader::getName)) + .collect(toList()); + + assertEquals(3, traders.size()); + assertEquals("Alan", traders.get(0).getName()); + assertEquals("Brian", traders.get(1).getName()); + assertEquals("Raoul", traders.get(2).getName()); + } + + @Test + public void allTraderNamesSortedAlphabetically() { + String result = transactions.stream() + .map(t -> t.getTrader().getName()) + .distinct() + .sorted() + .reduce("", (n1, n2) -> n1 + n2); + + assertEquals("AlanBrianMarioRaoul", result); + } + + @Test + public void anyTraderInMilan() { + boolean milanBased = transactions.stream() + .anyMatch(t -> t.getTrader().getCity().equals("Milan")); + + assertTrue(milanBased); + } + + @Test + public void highestTransactionValue() { + int highest = transactions.stream() + .map(Transaction::getValue) + .reduce(0, Integer::max); + + assertEquals(1000, highest); + } + + @Test + public void traderToString() { + Trader trader = new Trader("Test", "London"); + assertTrue(trader.toString().contains("Test")); + assertTrue(trader.toString().contains("London")); + } + + @Test + public void transactionToString() { + Trader trader = new Trader("Test", "London"); + Transaction tx = new Transaction(trader, 2020, 500); + String str = tx.toString(); + assertTrue(str.contains("2020")); + assertTrue(str.contains("500")); + } +}