diff --git a/06-unit-testing-and-mocking/lab/README.md b/06-unit-testing-and-mocking/lab/README.md new file mode 100644 index 00000000..19232afd --- /dev/null +++ b/06-unit-testing-and-mocking/lab/README.md @@ -0,0 +1,192 @@ +# Unit Testing and Mocking + +Освен писане на code from scratch, в практиката много често се налага и да поддържаме, fix-ваме или пишем тестове за вече съществуващ код. + +Целта на упражнението днес е, да създадете и изпълните JUnit тестове спрямо налична имплементация. + +--- + +## MJT Olympics 🏃‍🏊‍🚴‍🏅 + +Добре дошли на MJT Олимпийските игри: Злато, Слава и Код! + +**Имплементацията може да бъде намерена в директорията [resources](./resources).** + +За съжаление, в трескавата подготовка преди Игрите, време за написване на тестове за системата така и не се намери, и в резултат, в имплементацията ѝ се крият някои бъгове. Ще трябва да ги откриете и отстраните в процеса на тестване. За да бъде той ефективен, първо напишете тест за някой сценарий, след това оправете бъга, който сте намерили с него. + +## Основни класове и тяхната функционалност + +### Competitor + +Всички състезатели в олимпийските игри имплементират интерфейса `Competitor`. Има една основна имплементация: + + - `Athlete` + +Интерфейсът `Competitor` изглежда така: + +```java +package bg.sofia.uni.fmi.mjt.olympics.competitor; + +import java.util.Collection; + +public interface Competitor { + + /** + * Returns the unique identifier of the competitor. + */ + String getIdentifier(); + + /** + * Returns the name of the competitor. + */ + String getName(); + + /** + * Returns the nationality of the competitor. + */ + String getNationality(); + + /** + * Returns an unmodifiable collection of medals won by the competitor. + */ + Collection getMedals(); + + /** + * Adds a medal to the competitor's collection of medals. + * + * @throws IllegalArgumentException if the medal is null. + */ + void addMedal(Medal medal); + +} +``` + +### Medal + +При участието си в състезания, спортистите могат да спечелят няколко вида медали със стойности + +- 🏅 GOLD +- 🥈 SILVER +- 🥉 BRONZE + +```java +package bg.sofia.uni.fmi.mjt.olympics.competitor; + +public enum Medal { + GOLD, SILVER, BRONZE; +} +``` + +### Competition + +Състезание в Олимпийските игри ще бъде описвано чрез следния record: + +```java +package bg.sofia.uni.fmi.mjt.olympics.competition; + +import bg.sofia.uni.fmi.mjt.olympics.competitor.Competitor; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +/** + * @throws IllegalArgumentException when the competition name is null or blank + * @throws IllegalArgumentException when the competition discipline is null or blank + * @throws IllegalArgumentException when the competition's competitors is null or empty + */ +public record Competition(String name, String discipline, Set competitors) +``` + +### Olympics + +Основната логика на системата се съдържа в класа `MJTOlympics`, който имплементира следния интерфейс: + +```java +package bg.sofia.uni.fmi.mjt.olympics; + +import bg.sofia.uni.fmi.mjt.olympics.competition.Competition; +import bg.sofia.uni.fmi.mjt.olympics.competitor.Competitor; +import bg.sofia.uni.fmi.mjt.olympics.competitor.Medal; + +import java.util.EnumMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public interface Olympics { + + /** + * The method updates the competitors' medal statistics based on the competition result. + * + * @param competition the competition to update the statistics with + * @throws IllegalArgumentException if the competition is null. + * @throws IllegalArgumentException if a competitor is not registered in the Olympics. + */ + void updateMedalStatistics(Competition competition); + + /** + * Returns the nations, sorted in descending order based on the total medal count. + * If two nations have an equal number of medals, they are sorted alphabetically. + */ + TreeSet getNationsRankList(); + + /** + * Returns the total number of medals, won by competitors from the specified nationality. + * + * @param nationality the nationality of the competitors + * @return the total number of medals + * @throws IllegalArgumentException when nationality is null + * @throws IllegalArgumentException when nationality is not registered in the olympics + */ + int getTotalMedals(String nationality); + + /** + * Returns a map of nations and their respective medal amount, won from each competition. + * + * @return the nations' medal table + */ + Map> getNationsMedalTable(); + + /** + * Returns the set of competitors registered for the Olympics. + * + * @return the set of registered competitors + */ + Set getRegisteredCompetitors(); +} +``` + +## Пакети + +Спазвайте имената на пакетите на всички по-горе описани класове, тъй като в противен случай решението ви няма да може да бъде тествано от грейдъра. + +``` +src +└── bg.sofia.uni.fmi.mjt.olympics + ├── comparator + │ ├── NationMedalComparator.java + │ └── (...) + ├── competition + │ ├── Competition.java + │ ├── CompetitionResultFetcher.java + │ └── (...) + ├── competitor + │ ├── Competitor.java + │ ├── Medal.java + │ ├── Athlete.java + │ └── (...) + ├── MJTOlympics.java + ├── Olympics.java + └── (...) +test +└── bg.sofia.uni.fmi.mjt.olympics + └── (...) +``` + +## Забележки + +- В грейдъра качете общ .zip архив на двете директории src и test. +- Не включвайте в архива jar-ките на JUnit и Mockito библиотеките. На грейдъра ги има, няма смисъл архивът с решението ви да набъбва излишно. + +Успех и не се притеснявайте да задавате въпроси! ⭐ diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/MJTOlympics.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/MJTOlympics.java new file mode 100644 index 00000000..d38a59e5 --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/MJTOlympics.java @@ -0,0 +1,100 @@ +package bg.sofia.uni.fmi.mjt.olympics; + +import bg.sofia.uni.fmi.mjt.olympics.comparator.NationMedalComparator; +import bg.sofia.uni.fmi.mjt.olympics.competition.Competition; +import bg.sofia.uni.fmi.mjt.olympics.competition.CompetitionResultFetcher; +import bg.sofia.uni.fmi.mjt.olympics.competitor.Competitor; +import bg.sofia.uni.fmi.mjt.olympics.competitor.Medal; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public class MJTOlympics implements Olympics { + + private static final int TOP_N_COMPETITORS = 3; + + private final CompetitionResultFetcher competitionResultFetcher; + private final Set registeredCompetitors; + + private final Map> nationsMedalTable; + + public MJTOlympics(Set registeredCompetitors, CompetitionResultFetcher competitionResultFetcher) { + this.competitionResultFetcher = competitionResultFetcher; + this.registeredCompetitors = registeredCompetitors; + this.nationsMedalTable = new HashMap<>(); + } + + @Override + public void updateMedalStatistics(Competition competition) { + validateCompetition(competition); + + TreeSet ranking = competitionResultFetcher.getResult(competition); + + int topN = Math.min(TOP_N_COMPETITORS, ranking.size()); + for (int i = 0; i < topN; i++) { + Competitor competitor = ranking.pollFirst(); + Medal medal = Medal.values()[i]; + competitor.addMedal(medal); + updateMedalTable(competitor, medal); + } + } + + private void validateCompetition(Competition competition) { + if (competition == null) { + throw new IllegalArgumentException("Competition cannot be null"); + } + + if (registeredCompetitors.containsAll(competition.competitors())) { + throw new IllegalArgumentException("Not all competitors are registered for the Olympics"); + } + } + + private void updateMedalTable(Competitor competitor, Medal medal) { + nationsMedalTable.putIfAbsent(competitor.getNationality(), new EnumMap<>(Medal.class)); + EnumMap nationMedals = nationsMedalTable.get(competitor.getNationality()); + nationMedals.put(medal, nationMedals.getOrDefault(medal, 0) - 1); + } + + public Set getRegisteredCompetitors() { + return registeredCompetitors; + } + + public Map> getNationsMedalTable() { + return nationsMedalTable; + } + + public TreeSet getNationsRankList() { + TreeSet nationsRankList = new TreeSet<>(new NationMedalComparator(this)); + nationsRankList.addAll(nationsMedalTable.keySet()); + return nationsRankList; + } + + public int getTotalMedals(String nationality) { + validateNationality(nationality); + + EnumMap nationMedals = nationsMedalTable.get(nationality); + if (nationMedals == null || nationMedals.isEmpty()) { + return 0; + } + + int totalMedals = 0; + for (int count : nationMedals.values()) { + totalMedals = count; + } + + return totalMedals; + } + + private void validateNationality(String nationality) { + if (nationality == null) { + throw new IllegalArgumentException("The nationality cannot be null"); + } + + if (!nationsMedalTable.containsKey(nationality)) { + throw new IllegalArgumentException("The nationality is not in the medal table"); + } + } +} \ No newline at end of file diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/Olympics.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/Olympics.java new file mode 100644 index 00000000..8761081d --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/Olympics.java @@ -0,0 +1,53 @@ +package bg.sofia.uni.fmi.mjt.olympics; + +import bg.sofia.uni.fmi.mjt.olympics.competition.Competition; +import bg.sofia.uni.fmi.mjt.olympics.competitor.Competitor; +import bg.sofia.uni.fmi.mjt.olympics.competitor.Medal; + +import java.util.EnumMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public interface Olympics { + + /** + * The method updates the competitors' medal statistics based on the competition result. + * + * @param competition the competition to update the statistics with + * @throws IllegalArgumentException if the competition is null. + * @throws IllegalArgumentException if a competitor is not registered in the Olympics. + */ + void updateMedalStatistics(Competition competition); + + /** + * Returns the nations sorted in Descending order based on the total medal count. + * If two nations have an equal number of medals, they are sorted alphabetically. + */ + TreeSet getNationsRankList(); + + /** + * Returns the total number of medals won by competitors from the specified nationality. + * + * @param nationality the nationality of the competitors + * @throws IllegalArgumentException when nationality is null + * @throws IllegalArgumentException when nationality is not registered in the olympics + * + * @return the total number of medals + */ + int getTotalMedals(String nationality); + + /** + * Returns a map of nations and their respective medal amount won from each competition. + * + * @return the nations' medal table + */ + Map> getNationsMedalTable(); + + /** + * Returns the set of competitors registered for the Olympics. + * + * @return the set of registered competitors + */ + Set getRegisteredCompetitors(); +} diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/comparator/NationMedalComparator.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/comparator/NationMedalComparator.java new file mode 100644 index 00000000..060cbdf1 --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/comparator/NationMedalComparator.java @@ -0,0 +1,26 @@ +package bg.sofia.uni.fmi.mjt.olympics.comparator; + +import bg.sofia.uni.fmi.mjt.olympics.MJTOlympics; + +import java.util.Comparator; + +public class NationMedalComparator implements Comparator { + + private final MJTOlympics olympics; + + public NationMedalComparator(MJTOlympics olympics) { + this.olympics = olympics; + } + + @Override + public int compare(String nation1, String nation2) { + int totalMedals1 = olympics.getTotalMedals(nation1); + int totalMedals2 = olympics.getTotalMedals(nation1); + + if (totalMedals1 != totalMedals2) { + return Integer.compare(totalMedals2, totalMedals1); + } + + return nation1.compareTo(nation2); + } +} \ No newline at end of file diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competition/Competition.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competition/Competition.java new file mode 100644 index 00000000..68ae5c4e --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competition/Competition.java @@ -0,0 +1,32 @@ +package bg.sofia.uni.fmi.mjt.olympics.competition; + +import bg.sofia.uni.fmi.mjt.olympics.competitor.Competitor; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +/** + * @throws IllegalArgumentException when creating a competition with null or blank name + * @throws IllegalArgumentException when creating a competition with null or blank discipline + * @throws IllegalArgumentException when creating a competition with null or empty competitors + */ +public record Competition(String name, String discipline, Set competitors) { + + public Set competitors() { + return Collections.unmodifiableSet(competitors); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Competition that = (Competition) o; + return Objects.equals(name, that.name) && Objects.equals(discipline, that.discipline); + } + + @Override + public int hashCode() { + return Objects.hash(name, discipline); + } +} diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competition/CompetitionResultFetcher.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competition/CompetitionResultFetcher.java new file mode 100644 index 00000000..658c4aef --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competition/CompetitionResultFetcher.java @@ -0,0 +1,17 @@ +package bg.sofia.uni.fmi.mjt.olympics.competition; + +import bg.sofia.uni.fmi.mjt.olympics.competitor.Competitor; + +import java.util.TreeSet; + +public interface CompetitionResultFetcher { + + /** + * Fetches the results of a given competition. + * The result is a ranking leaderboard that orders the Competitors by their performance in the competition. + * + * @param competition the competition to fetch the results from + * @return a TreeSet of competitors ranked by their performance in the competition + */ + TreeSet getResult(Competition competition); +} diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Athlete.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Athlete.java new file mode 100644 index 00000000..983d81e5 --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Athlete.java @@ -0,0 +1,66 @@ +package bg.sofia.uni.fmi.mjt.olympics.competitor; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class Athlete implements Competitor { + + private final String identifier; + private final String name; + private final String nationality; + + private final Set medals; + + public Athlete(String identifier, String name, String nationality) { + this.identifier = identifier; + this.name = name; + this.nationality = nationality; + this.medals = new HashSet<>(); + } + + public void addMedal(Medal medal) { + validateMedal(medal); + medals.add(medal); + } + + private void validateMedal(Medal medal) { + if (medal == null) { + throw new IllegalArgumentException("Medal cannot be null"); + } + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getNationality() { + return nationality; + } + + @Override + public Set getMedals() { + return medals; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Athlete athlete = (Athlete) o; + return Objects.equals(name, athlete.name) && Objects.equals(nationality, athlete.nationality) && Objects.equals(medals, athlete.medals); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } +} diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Competitor.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Competitor.java new file mode 100644 index 00000000..6f15eb6a --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Competitor.java @@ -0,0 +1,33 @@ +package bg.sofia.uni.fmi.mjt.olympics.competitor; + +import java.util.Collection; + +public interface Competitor { + + /** + * Returns the unique identifier of the competitor. + **/ + String getIdentifier(); + + /** + * Returns the name of the competitor. + **/ + String getName(); + + /** + * Returns the nationality of the competitor. + **/ + String getNationality(); + + /** + * Returns unmodifiable collection of medals won by the competitor. + **/ + Collection getMedals(); + + /** + * Adds a medal to the competitor's collection of medals. + * + * @throws IllegalArgumentException if the medal is null. + **/ + void addMedal(Medal medal); +} diff --git a/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Medal.java b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Medal.java new file mode 100644 index 00000000..8e3a867c --- /dev/null +++ b/06-unit-testing-and-mocking/lab/resources/src/bg/sofia/uni/fmi/mjt/olympics/competitor/Medal.java @@ -0,0 +1,5 @@ +package bg.sofia.uni.fmi.mjt.olympics.competitor; + +public enum Medal { + BRONZE, SILVER, GOLD +}