top10: 1.1.2 - (RED) Explicitly formulated expectations for `Ranking`
authorKai Moritz <kai@juplo.de>
Sat, 25 May 2024 08:17:57 +0000 (10:17 +0200)
committerKai Moritz <kai@juplo.de>
Thu, 30 May 2024 10:03:39 +0000 (12:03 +0200)
src/main/java/de/juplo/kafka/wordcount/top10/Ranking.java
src/test/java/de/juplo/kafka/wordcount/top10/RankingTest.java [new file with mode: 0644]

index 80e8742..0635384 100644 (file)
@@ -12,6 +12,9 @@ import java.util.List;
 @Data
 public class Ranking
 {
+  public final static int MAX_ENTRIES = 10;
+
+
   private Entry[] entries = new Entry[0];
 
   public Ranking add(Entry newEntry)
@@ -41,9 +44,9 @@ public class Ranking
             break;
           }
         }
-        if (list.size() > 10)
+        if (list.size() > MAX_ENTRIES)
         {
-          list = list.subList(0,10);
+          list = list.subList(0, MAX_ENTRIES);
         }
         entries = list.toArray(num -> new Entry[num]);
         return this;
diff --git a/src/test/java/de/juplo/kafka/wordcount/top10/RankingTest.java b/src/test/java/de/juplo/kafka/wordcount/top10/RankingTest.java
new file mode 100644 (file)
index 0000000..e92d87c
--- /dev/null
@@ -0,0 +1,230 @@
+package de.juplo.kafka.wordcount.top10;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+
+
+public class RankingTest
+{
+  @DisplayName("A newly created instance is empty")
+  @Test
+  public void testNewRankingIsEmpty()
+  {
+    Ranking ranking = new Ranking();
+    assertThat(ranking.getEntries()).isEmpty();
+  }
+
+  @DisplayName("An instance that was build from an empty ranking is empty")
+  @Test
+  public void testRankingOfYieldsExpectedResultForEmptyList()
+  {
+    Ranking ranking = new Ranking();
+    assertThat(ranking.getEntries()).isEmpty();
+  }
+
+  @DisplayName("An instance that was build from a valid ranking contains the expected entries")
+  @ParameterizedTest
+  @MethodSource("validRankingsProvider")
+  public void testRankingOfYieldsExpectedResultsForValidRankings(List<Entry> entryList)
+  {
+    Ranking ranking = Ranking.of(toArray(entryList));
+    assertThat(ranking.getEntries()).containsExactlyElementsOf(entryList);
+  }
+
+  @DisplayName("The builder fails for invalid rankings")
+  @ParameterizedTest
+  @MethodSource("invalidRankingsProvider")
+  public void testRankingOfThrowsExceptionForInvalidRankings(List<Entry> entryList)
+  {
+    assertThatExceptionOfType(IllegalArgumentException.class)
+        .isThrownBy(() -> Ranking.of(toArray(entryList)));
+  }
+
+  @DisplayName("Adding a new word with highest ranking, pushes all other words down")
+  @ParameterizedTest
+  @MethodSource("validRankingsProvider")
+  public void testAddingNewWordWithHighestRanking(List<Entry> entryList)
+  {
+    Ranking ranking = Ranking.of(toArray(entryList));
+    Entry newEntry = Entry.of("NEW!", rankingForPosition(-1));
+    ranking.add(newEntry);
+    assertThat(ranking.getEntries()[0]).isEqualTo(newEntry);
+    for (int i = 0; i < entryList.size() && i < Ranking.MAX_ENTRIES - 1; i++)
+    {
+      assertThat(ranking.getEntries()[i + 1]).isEqualTo(entryList.get(i));
+    }
+  }
+
+  @DisplayName("Adding a new word with an existent ranking, pushes all words with lower ranking down")
+  @ParameterizedTest
+  @MethodSource("validRankingsProvider")
+  public void testAddingNewWordWithExistingRanking(List<Entry> entryList)
+  {
+    for (int position = 0; position < entryList.size(); position++ )
+    {
+      Ranking ranking = Ranking.of(toArray(entryList));
+      Entry newEntry = Entry.of("NEW!", rankingForPosition(position));
+      ranking.add(newEntry);
+      for (int i = 0; i < entryList.size() && i < Ranking.MAX_ENTRIES - 1; i++)
+      {
+        if (i < position)
+        {
+          assertThat(ranking.getEntries()[i]).isEqualTo(entryList.get(i));
+        }
+        if (i == position)
+        {
+          assertThat(ranking.getEntries()[i]).isEqualTo(entryList.get(i));
+          assertThat(ranking.getEntries()[i + 1]).isEqualTo(newEntry);
+        }
+        if (i > position)
+        {
+          assertThat(ranking.getEntries()[i + 1]).isEqualTo(entryList.get(i));
+        }
+      }
+    }
+  }
+
+  @DisplayName("Adding a highest ranking for an existing word shifts it to the first place")
+  @ParameterizedTest
+  @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })
+  public void testAddingExistingWordWithHighestRanking(int position)
+  {
+    Ranking ranking = Ranking.of(toArray(VALID_RANKINGS[0]));
+    String word = wordForPosition(position);
+    Entry highestEntry = Entry.of(word, 100l);
+    ranking.add(highestEntry);
+    List<Entry> expectedEntries = Stream
+        .concat(
+            Stream.of(highestEntry),
+            VALID_RANKINGS[0]
+                .stream()
+                .filter(entry -> !entry.getWord().equals(word)))
+        .toList();
+    assertThat(ranking.getEntries()).containsExactlyElementsOf(expectedEntries);
+  }
+
+  @DisplayName("Adding an existing word with unchanged ranking changes nothing")
+  @ParameterizedTest
+  @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 })
+  public void testAddingExistingWordWithUnchangedRanking(int position)
+  {
+    Ranking ranking = Ranking.of(toArray(VALID_RANKINGS[0]));
+    Entry unchangedEntry = Entry.of(
+        wordForPosition(position),
+        rankingForPosition(position));
+    ranking.add(unchangedEntry);
+    assertThat(ranking.getEntries()).containsExactlyElementsOf(VALID_RANKINGS[0]);
+  }
+
+  @DisplayName("Adding an existing word with a lower ranking fails")
+  @ParameterizedTest
+  @MethodSource("validRankingsProvider")
+  public void testAddingExistingWordWithLowerRankingFails(List<Entry> entryList)
+  {
+    Ranking ranking = Ranking.of(toArray(entryList));
+    entryList.forEach(entry ->
+      assertThatExceptionOfType(IllegalArgumentException.class)
+          .isThrownBy(() -> ranking.add(Entry.of(entry.getWord(), entry.getCounter() - 1))));
+  }
+
+
+  Entry[] toArray(List<Entry> entryList)
+  {
+    return entryList.toArray(size -> new Entry[size]);
+  }
+
+  static String wordForPosition(int position)
+  {
+    return Integer.toString(position+1);
+  }
+
+  static long rankingForPosition(int position)
+  {
+    return (long)Ranking.MAX_ENTRIES * 2 - position;
+  }
+
+  static Stream<List<Entry>> validRankingsProvider()
+  {
+    return Stream.of(VALID_RANKINGS);
+  }
+
+  static Stream<List<Entry>> invalidRankingsProvider()
+  {
+    return Stream.of(INVALID_RANKINGS);
+  }
+
+  static String[] WORDS = new String[Ranking.MAX_ENTRIES];
+  static List<Entry>[] VALID_RANKINGS = new List[Ranking.MAX_ENTRIES];
+
+  static
+  {
+    for (int i = 0; i < Ranking.MAX_ENTRIES; i++)
+    {
+      List<Entry> ranking = new LinkedList<>();
+      String word = null;
+      for (int position = 0; position <= i; position++)
+      {
+        word = wordForPosition(position);
+        Entry entry = Entry.of(word, rankingForPosition(position));
+        ranking.add(entry);
+      }
+      WORDS[i] = word;
+      VALID_RANKINGS[Ranking.MAX_ENTRIES - (i + 1)] = ranking;
+    }
+  }
+
+  static List<Entry>[] INVALID_RANKINGS = new List[] {
+      List.of(
+          Entry.of("Platz eins", 1l),
+          Entry.of("Platz zwei", 2l)),
+      List.of(
+          Entry.of("Platz eins", 1111111111l),
+          Entry.of("Platz zwei", 222222222l),
+          Entry.of("Platz eins", 1l)),
+      List.of(
+          Entry.of("Platz eins", 11l),
+          Entry.of("Platz eins", 1l)),
+      List.of(
+          Entry.of("Platz eins", 1111111111l),
+          Entry.of("Platz zwei", 222222222l),
+          Entry.of("Platz eins", 11111111l),
+          Entry.of("Platz zwei", 2222222l),
+          Entry.of("Platz fünf", 555555l)),
+      List.of(
+          Entry.of("Platz eins", 1111111111l),
+          Entry.of("Platz zwei", 222222222l),
+          Entry.of("Platz drei", 33333333l),
+          Entry.of("Platz vier", 4444444l),
+          Entry.of("Platz eins", 111111l),
+          Entry.of("Platz sechs", 66666l)),
+      List.of(
+          Entry.of("Platz eins", 1111111111l),
+          Entry.of("Platz zwei", 222222222l),
+          Entry.of("Platz drei", 33333333l),
+          Entry.of("Platz vier", 4444444l),
+          Entry.of("Platz fünf", 555555l),
+          Entry.of("Platz sechs", 66666l),
+          Entry.of("Platz eins", 1l)),
+      List.of(
+          Entry.of("Platz eins", 1111111111l),
+          Entry.of("Platz zwei", 222222222l),
+          Entry.of("Platz drei", 33333333l),
+          Entry.of("Platz vier", 4444444l),
+          Entry.of("Platz fünf", 555555l),
+          Entry.of("Platz sechs", 66666l),
+          Entry.of("Platz sieben", 7777l),
+          Entry.of("Platz acht", 888l),
+          Entry.of("Platz neun", 99l),
+          Entry.of("Platz 10", 6l),
+          Entry.of("Platz 11", 3l))};
+}