diff --git a/.gitignore b/.gitignore index 5ff6309..0d5994f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,8 @@ target/ !**/src/test/**/target/ ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ +.idea +.run *.iws *.iml *.ipr diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/jpa-buddy.xml b/.idea/jpa-buddy.xml deleted file mode 100644 index 966d5f5..0000000 --- a/.idea/jpa-buddy.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 5c936ee..b26576a 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ -# Практическое задание 2 # Redis Map +## Prerequisites +- JDK 17+ +- Apache Maven 3.9+ +- Docker -В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map -Достаточно будет реализации Map - -**Описание:**
-✅ Написать реализацию методов Map, которая будет работать с Redis
-✅ Работать с redis можно через Jedis
-✅ Покрыть все методы тестами
-✅ Для поднятия redis, в корне проекта есть docker-compose файл
- -Redis поднимается командой +## Getting started +```bash +git clone https://github.com/swankygawk/RedisMap.git +cd RedisMap ``` + +## Launching demo +```bash docker-compose up -d +mvn compile exec:java -Dexec.mainClass="org.redis.Application" ``` -**Как выполнить:** -1. Сделать форк этого проекта -2. Сделать ветку dev в своем проекте. Выполнять работу в ней -3. После выполнения работы открыть Pull Request в master -4. Назначить ментора ревьювером -5. Отправить ссылку на PR в тг чат ментору \ No newline at end of file +## Launching tests +```bash +mvn test +``` +Tests use testcontainers, so make sure to have a running Docker environment diff --git a/TASKREADME.md b/TASKREADME.md new file mode 100644 index 0000000..5c936ee --- /dev/null +++ b/TASKREADME.md @@ -0,0 +1,22 @@ +# Практическое задание 2 # Redis Map + +В рамках практической задачи нужно реализовать прослойку между Redis и интерфейсом Map +Достаточно будет реализации Map + +**Описание:**
+✅ Написать реализацию методов Map, которая будет работать с Redis
+✅ Работать с redis можно через Jedis
+✅ Покрыть все методы тестами
+✅ Для поднятия redis, в корне проекта есть docker-compose файл
+ +Redis поднимается командой +``` +docker-compose up -d +``` + +**Как выполнить:** +1. Сделать форк этого проекта +2. Сделать ветку dev в своем проекте. Выполнять работу в ней +3. После выполнения работы открыть Pull Request в master +4. Назначить ментора ревьювером +5. Отправить ссылку на PR в тг чат ментору \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 23de504..701f295 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,9 +1,7 @@ -version: '3.3' - services: redis: container_name: redis - image: redis:latest + image: redis:8.2.1 restart: always ports: - "6379:6379" \ No newline at end of file diff --git a/pom.xml b/pom.xml index b4c0aac..0364e6e 100644 --- a/pom.xml +++ b/pom.xml @@ -15,15 +15,68 @@ + + org.projectlombok + lombok + 1.18.42 + provided + + + + com.google.guava + guava + 33.5.0-jre + redis.clients jedis - 5.1.5 + 6.2.0 + + + + org.slf4j + slf4j-simple + 1.7.36 + test org.junit.jupiter junit-jupiter - 5.8.1 + 5.13.4 + test + + + org.assertj + assertj-core + 3.27.6 + test + + + org.mockito + mockito-junit-jupiter + 5.20.0 + test + + + + + org.testcontainers + testcontainers + 1.21.3 + test + + + org.testcontainers + junit-jupiter + 1.21.3 + test + + + + + com.redis.testcontainers + testcontainers-redis + 1.6.4 test diff --git a/src/main/java/org/redis/Application.java b/src/main/java/org/redis/Application.java new file mode 100644 index 0000000..844953b --- /dev/null +++ b/src/main/java/org/redis/Application.java @@ -0,0 +1,80 @@ +package org.redis; + +import redis.clients.jedis.Jedis; + +import java.util.Map; + +@SuppressWarnings({"CallToPrintStackTrace", "ConstantValue"}) +public final class Application { + public static void main(String[] args) { + try (Jedis jedis = new Jedis("localhost", 6379)) { + Map userCache = new RedisMap(jedis, "users-cache"); + userCache.clear(); + + System.out.println("\n--- Демонстрация RedisMap ---"); + System.out.println("\nНачальное состояние: isEmpty: " + userCache.isEmpty() + ", Размер: " + userCache.size()); + + // -------------------------------------------------------------------------- + + System.out.println("\n1. Добавление пользователей"); + + userCache.put("user:1", "Alice"); + userCache.put("user:2", "Bob"); + String previousValue = userCache.put("user:3", "Charlie"); + + System.out.println("put(\"user:3\", \"Charlie\") вернул: " + previousValue); + System.out.println("Текущий размер: " + userCache.size()); + + // -------------------------------------------------------------------------- + + System.out.println("\n2. Получение данных"); + + String user1 = userCache.get("user:1"); + + System.out.println("Значение для \"user:1\": " + user1); + System.out.println("Есть ли ключ \"user:2\"? " + userCache.containsKey("user:2")); + System.out.println("Есть ли значение \"Diana\"? " + userCache.containsValue("Diana")); + + // -------------------------------------------------------------------------- + + System.out.println("\n3. Обновление пользователя \"user:3\""); + + previousValue = userCache.put("user:3", "Charles"); + + System.out.println("put(\"user:3\", \"Charles\") вернул старое значение: " + previousValue); + System.out.println("Новое значение для \"user:3\": " + userCache.get("user:3")); + + // -------------------------------------------------------------------------- + + System.out.println("\n4. Все пользователи:"); + + for (Map.Entry entry : userCache.entrySet()) { + System.out.printf(" - Ключ: %s, Значение: %s%n", entry.getKey(), entry.getValue()); + } + + // -------------------------------------------------------------------------- + + System.out.println("\n5. Удаление пользователя \"user:2\""); + + String removedUser = userCache.remove("user:2"); + + System.out.println("Удаленное значение: " + removedUser); + System.out.println("Размер после удаления: " + userCache.size()); + + // -------------------------------------------------------------------------- + + System.out.println("\n6. Очистка"); + + userCache.clear(); + + System.out.println("Финальное состояние: isEmpty: " + userCache.isEmpty() + ", Размер: " + userCache.size()); + + // -------------------------------------------------------------------------- + + System.out.println("\n--- Окончание демонстрации ---"); + } catch (Exception e) { + System.err.println("Ошибка при работе с Redis: " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/redis/Main.java b/src/main/java/org/redis/Main.java deleted file mode 100644 index 68b3db6..0000000 --- a/src/main/java/org/redis/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.redis; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} \ No newline at end of file diff --git a/src/main/java/org/redis/RedisMap.java b/src/main/java/org/redis/RedisMap.java new file mode 100644 index 0000000..dd26b4d --- /dev/null +++ b/src/main/java/org/redis/RedisMap.java @@ -0,0 +1,279 @@ +package org.redis; + +import com.google.common.annotations.VisibleForTesting; +import lombok.NonNull; +import redis.clients.jedis.Jedis; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * Реализация интерфейса {@link Map} для строк, использующая Redis в качестве хранилища + *

+ * Все пары ключ-значение хранятся в одной структуре данных Redis Hash, + * имя которой задаётся через конструктор + *

+ * Особенности реализации: + *
    + *
  • Не поддерживает {@code null} в качестве ключей и значений. + * При попытке использовать {@code null} будет выброшено исключение {@code NullPointerException}
  • + *
  • Методы, возвращающие коллекции ({@link #keySet()}, {@link #values()}, {@link #entrySet()}), + * возвращают копию данных на момент вызова, а не "живое" представление
  • + *
  • Потокобезопасность этого класса зависит от потокобезопасности переданного клиента {@link Jedis}. + * Стандартные экземпляры {@code Jedis} не являются потокобезопасными
  • + *
+ */ +@SuppressWarnings("ClassCanBeRecord") +public class RedisMap implements Map { + private final Jedis jedis; + private final String mapKey; + + /** + * Создаёт новый экземпляр {@code RedisMap}, связанный с определённым ключом в Redis + * + * @param jedis клиент Jedis для взаимодействия с Redis + * @param mapKey ключ, под которым в Redis будет храниться хэш + * + * @throws NullPointerException если {@code jedis} или {@code mapKey} являются {@code null} + */ + public RedisMap(Jedis jedis, String mapKey) { + this.jedis = checkNotNull(jedis, "jedis client cannot be null"); + this.mapKey = checkNotNull(mapKey, "mapKey cannot be null"); + } + + /** + * Возвращает количество пар ключ-значение в этом {@code RedisMap} + * + * @return количество пар ключ-значение + * + * @throws IllegalStateException если количество элементов в Redis превышает {@link Integer#MAX_VALUE} + */ + @Override + public int size() { + long realSize = jedis.hlen(mapKey); + checkState( + realSize <= Integer.MAX_VALUE, + "RedisMap size exceeds Integer.MAX_VALUE: " + realSize + ); + + return (int) realSize; + } + + /** + * Проверяет, содержит ли этот {@code RedisMap} пары ключ-значение + * + * @return {@code true} если RedisMap пуст + */ + @Override + public boolean isEmpty() { + return jedis.hlen(mapKey) == 0L; + } + + /** + * Проверяет, содержит ли этот {@code RedisMap} сопоставление для указанного ключа + * + * @param key ключ, наличие которого необходимо проверить + * + * @return • {@code true} если сопоставление существует + *
+ * • {@code false} если сопоставления не существует или если {@code key} не является {@code String} + */ + @Override + public boolean containsKey(Object key) { + if (!(key instanceof String)) { + return false; // работает и с key == null + } + + return jedis.hexists(mapKey, (String) key); + } + + /** + * Проверяет, сопоставляет ли этот {@code RedisMap} один или несколько ключей с указанным значением + * + * @param value значение, наличие которого необходимо проверить + * + * @return • {@code true} если сопоставление существует + *
+ * • {@code false} если сопоставления не существует или если {@code value} не является {@code String} + */ + @Override + public boolean containsValue(Object value) { + if (!(value instanceof String)) { + return false; // работает и с value == null + } + + // может быть медленным для больших мап + // в таком случае можно использовать итеративный HSCAN + return jedis.hvals(mapKey).contains((String) value); + } + + /** + * Получает значение, с которым сопоставлен указанный ключ + * + * @param key ключ, сопоставленное значение которого нужно получить + * + * @return • полученное значение + *
+ * • {@code null} если сопоставления нет или если {@code key} не является {@code String} + */ + @Override + public String get(Object key) { + if (!(key instanceof String)) { + return null; // работает и с key == null + } + + return jedis.hget(mapKey, (String) key); + } + + /** + * Связывает указанное значение с указанным ключом в этом {@code RedisMap} + * + * @param key ключ, с которым должно быть связано указанное значение + * @param value значение, которое должно быть связано с указанным ключом + * + * @return • предыдущее значение, связанное с {@code key} + *
+ * • {@code null} если его не было + * + * @throws NullPointerException если {@code key} или {@code value} являются {@code null} + */ + @Override + public String put(String key, String value) { + checkNotNull(key, "key cannot be null"); + checkNotNull(value, "value cannot be null"); + + String oldValue = jedis.hget(mapKey, key); + jedis.hset(mapKey, key, value); + return oldValue; + } + + /** + * Удаляет сопоставление для ключа из этого {@code RedisMap}, если оно существует + * + * @param key ключ, чьё значение должно быть удалено + * + * @return • значение, которое было сопоставлено с указанным ключом + *
+ * • {@code null} если сопоставления не было или если {@code key} не является {@code String} + */ + @Override + public String remove(Object key) { + if (!(key instanceof String keyString)) { + return null; // работает и с key == null + } + + String oldValue = jedis.hget(mapKey, keyString); + if (oldValue != null) { + jedis.hdel(mapKey, keyString); + } + + return oldValue; + } + + /** + * Копирует все сопоставления из указанного {@code Map} в этот {@code RedisMap}. + *
+ * Атомарная операция: либо все элементы добавляются успешно, либо, в случае ошибки, + * не будет добавлен ни один элемент + * + * @param m {@code Map}, элементы которого будут добавлены + * + * @throws NullPointerException + *
+ * • если {@code m} является {@code null} + *
+ * • если любой из ключей в {@code m} является {@code null} + *
+ * • если любое из значений в {@code m} является {@code null} + */ + @Override + @SuppressWarnings("NullableProblems") // проверяем m на null вручную + public void putAll(Map m) { + checkNotNull(m, "m cannot be null"); + + if (m.isEmpty()) { + return; + } + + for (Map.Entry entry : m.entrySet()) { + checkNotNull(entry.getKey(), "key in m cannot be null"); + checkNotNull(entry.getValue(), "value in m cannot be null"); + } + + jedis.hset(mapKey, new HashMap<>(m)); + } + + /** + * Удаляет все сопоставления из этого {@code RedisMap} + */ + @Override + public void clear() { + jedis.del(mapKey); + } + + // ------------------------------------------------------------------------------------- + // Следующие методы возвращают снимок данных (копию), а не "живое" представление (view) + // Изменения в возвращенной коллекции не повлияют на данные в Redis, и наоборот + + /** + * Возвращает {@link Set} ключей, содержащихся в этом {@code RedisMap} + *

+ * Возвращаемый {@code Set} является копией данных из Redis на момент вызова. + * Изменения в этом {@code Set} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap} + * не отразятся в ранее полученном {@code Set} + * + * @return множество ключей из этого {@code RedisMap} + */ + @Override + @NonNull + public Set keySet() { + return jedis.hkeys(mapKey); + } + + /** + * Возвращает {@link Collection} значений, содержащихся в этом {@code RedisMap} + *

+ * Возвращаемый {@code Collection} является копией данных из Redis на момент вызова. + * Изменения в этом {@code Collection} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap} + * не отразятся в ранее полученном {@code Collection} + * + * @return коллекция значений из этого {@code RedisMap} + */ + @Override + @NonNull + public Collection values() { + return jedis.hvals(mapKey); + } + + /** + * Возвращает {@link Set} сопоставлений ключ-значение, содержащихся в этом {@code RedisMap} + *

+ * Возвращаемый {@code Set} является копией данных из Redis на момент вызова. + * Изменения в этом {@code Set} не отразятся в этом {@code RedisMap}, а изменения в этом {@code RedisMap} + * не отразятся в ранее полученном {@code Set} + * + * @return множество сопоставлений ключ-значение из этого {@code RedisMap} + */ + @Override + @NonNull + public Set> entrySet() { + return jedis.hgetAll(mapKey).entrySet(); + } + // ------------------------------------------------------------------------------------- + + + /** + * Удаляет весь хэш из Redis. + *

+ * Предназначен только для использования в тестах для очистки состояния + */ + @VisibleForTesting + void destroyForTesting() { + jedis.del(mapKey); + } +} diff --git a/src/test/java/org/redis/RedisMapTest.java b/src/test/java/org/redis/RedisMapTest.java new file mode 100644 index 0000000..5ae6765 --- /dev/null +++ b/src/test/java/org/redis/RedisMapTest.java @@ -0,0 +1,300 @@ +package org.redis; + +import com.redis.testcontainers.RedisContainer; +import org.junit.jupiter.api.*; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import redis.clients.jedis.Jedis; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Testcontainers +@SuppressWarnings("ConstantValue") +public class RedisMapTest { + @Container + private static final RedisContainer redisContainer = new RedisContainer( + RedisContainer.DEFAULT_IMAGE_NAME.withTag("8.2.1") + ); + + private static Jedis jedis; + private static final String TEST_MAP_KEY = "test:redis:map"; + + private RedisMap redisMap; + + @BeforeAll + static void setUpAll() { + String redisUri = redisContainer.getRedisURI(); + jedis = new Jedis(redisUri); + } + + @BeforeEach + void setUp() { + redisMap = new RedisMap(jedis, TEST_MAP_KEY); + redisMap.destroyForTesting(); + } + + @AfterAll + static void tearDownAll() { + jedis.close(); + } + + @Test + @DisplayName("get должен возвращать null, если key не String") + void getShouldReturnNull_whenKeyIsNotString() { + // Act & Assert + assertThat(redisMap.get(null)).isNull(); + } + + @Test + @DisplayName("put должен выбрасывать NPE, если key null") + void putShouldThrowNPE_whenKeyIsNull() { + // Act & Assert + assertThatThrownBy(() -> redisMap.put(null, "value")) + .isInstanceOf(NullPointerException.class) + .hasMessage("key cannot be null"); + } + + @Test + @DisplayName("put должен выбрасывать NPE, если value null") + void putShouldThrowNPE_whenValueIsNull() { + // Act & Assert + assertThatThrownBy(() -> redisMap.put("key", null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("value cannot be null"); + } + + @Test + @DisplayName("Должен класть и получать значение") + void shouldPutAndGetValue() { + // Act + redisMap.put("key1", "value1"); + String retrievedValue = redisMap.get("key1"); + + // Assert + assertThat(retrievedValue).isEqualTo("value1"); + } + + + @Test + @DisplayName("put должен возвращать предыдущее значение") + void putShouldReturnPreviousValue() { + // Arrange + redisMap.put("key1", "value1"); + + // Act + String previousValue = redisMap.put("key1", "newValue"); + + // Assert + assertThat(previousValue).isEqualTo("value1"); + assertThat(redisMap.get("key1")).isEqualTo("newValue"); + } + + @Test + @DisplayName("Должен корректно считать размер и проверять на пустоту") + void shouldCorrectlyCalculateSizeAndIsEmpty() { + // Arrange & Assert + assertThat(redisMap.isEmpty()).isTrue(); + assertThat(redisMap.size()).isZero(); + + // Act + redisMap.put("key1", "value1"); + + // Assert + assertThat(redisMap.isEmpty()).isFalse(); + assertThat(redisMap.size()).isEqualTo(1); + } + + @Test + @DisplayName("containsKey должен возвращать false, если key не String") + void containsKeyShouldReturnFalse_whenKeyIsNotString() { + // Act & Assert + assertThat(redisMap.containsKey(null)).isFalse(); + } + + @Test + @DisplayName("containsValue должен возвращать false, если value не String") + void containsValueShouldReturnFalse_whenValueIsNotString() { + // Act & Assert + assertThat(redisMap.containsValue(null)).isFalse(); + } + + @Test + @DisplayName("Должен проверять наличие ключа и значения") + void shouldCheckContainsKeyAndValue() { + // Arrange + redisMap.put("key1", "value1"); + + // Assert + assertThat(redisMap.containsKey("key1")).isTrue(); + assertThat(redisMap.containsKey("non-existent-key")).isFalse(); + + assertThat(redisMap.containsValue("value1")).isTrue(); + assertThat(redisMap.containsValue("non-existent-value")).isFalse(); + } + + @Test + @DisplayName("remove должен возвращать null, если key не String") + void removeShouldReturnNull_whenKeyIsNotString() { + // Act & Assert + assertThat(redisMap.remove(null)).isNull(); + } + + @Test + @DisplayName("remove должен возвращать null, если для key нет сопоставления") + void removeShouldReturnNull_whenKeyDoesNotExist() { + // Act & Assert + assertThat(redisMap.remove("key")).isNull(); + } + + @Test + @DisplayName("Должен удалять элемент и возвращать его значение") + void shouldRemoveElementAndReturnValue() { + // Arrange + redisMap.put("key1", "value1"); + + // Act + String removedValue = redisMap.remove("key1"); + + // Assert + assertThat(removedValue).isEqualTo("value1"); + assertThat(redisMap.size()).isZero(); + assertThat(redisMap.isEmpty()).isTrue(); + assertThat(redisMap.containsKey("key1")).isFalse(); + assertThat(redisMap.containsValue("value1")).isFalse(); + } + + @Test + @DisplayName("putAll должен добавлять все элементы из другой Map") + void shouldPutAllElementsFromOtherMap() { + // Arrange + Map map = Map.of( + "key1", "value1", + "key2", "value2", + "key3", "value3" + ); + + // Act + redisMap.putAll(map); + + // Assert + assertThat(redisMap.size()).isEqualTo(3); + assertThat(redisMap.entrySet()).containsAll(map.entrySet()); + } + + @Test + @DisplayName("putAll должен выбрасывать NPE, если m null") + void putAllShouldThrowNPE_whenMIsNull() { + // Act & Assert + assertThatThrownBy(() -> redisMap.putAll(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("m cannot be null"); + } + + @Test + @DisplayName("putAll должен выбрасывать NPE, если в m есть null ключ") + void putAllShouldThrowNPE_whenMContainsNullKey() { + // Arrange + Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put(null, "value2"); + + // Act & Assert + assertThatThrownBy(() -> redisMap.putAll(map)) + .isInstanceOf(NullPointerException.class) + .hasMessage("key in m cannot be null"); + assertThat(redisMap.isEmpty()).isTrue(); // check for atomicity + } + + @Test + @DisplayName("putAll должен выбрасывать NPE, если в m есть null значение") + void putAllShouldThrowNPE_whenMContainsNullValue() { + // Arrange + Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", null); + + // Act & Assert + assertThatThrownBy(() -> redisMap.putAll(map)) + .isInstanceOf(NullPointerException.class) + .hasMessage("value in m cannot be null"); + assertThat(redisMap.isEmpty()).isTrue(); // check for atomicity + } + + @Test + @DisplayName("putAll не должен ничего делать, если m.isEmpty()") + void putAllShouldDoNothing_whenMIsEmpty() { + // Arrange + Map map = Collections.emptyMap(); + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + // Act + redisMap.putAll(map); + + // Assert + assertThat(redisMap.size()).isEqualTo(2); + } + + @Test + @DisplayName("Должен очищать всю RedisMap") + void shouldClearMap() { + // Arrange + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + // Act + redisMap.clear(); + + // Assert + assertThat(redisMap.isEmpty()).isTrue(); + } + + @Test + @DisplayName("Должен возвращать корректный keySet") + void shouldReturnCorrectKeySet() { + // Arrange + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + // Act + Set keySet = redisMap.keySet(); + + // Assert + assertThat(keySet).containsExactlyInAnyOrder("key1", "key2"); + } + + @Test + @DisplayName("Должен возвращать корректный values") + void shouldReturnCorrectValues() { + // Arrange + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + redisMap.put("key3", "value1"); + + // Act + Collection values = redisMap.values(); + + // Assert + assertThat(values).containsExactlyInAnyOrder("value1", "value2", "value1"); + } + + @Test + @DisplayName("Должен возвращать корректный entrySet") + void shouldReturnCorrectEntrySet() { + // Arrange + redisMap.put("key1", "value1"); + redisMap.put("key2", "value2"); + + // Act + Set> entrySet = redisMap.entrySet(); + + // Assert + assertThat(entrySet).containsExactlyInAnyOrder( + Map.entry("key1", "value1"), + Map.entry("key2", "value2") + ); + } +} diff --git a/src/test/java/org/redis/RedisMapUnitTest.java b/src/test/java/org/redis/RedisMapUnitTest.java new file mode 100644 index 0000000..52d1787 --- /dev/null +++ b/src/test/java/org/redis/RedisMapUnitTest.java @@ -0,0 +1,35 @@ +package org.redis; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import redis.clients.jedis.Jedis; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RedisMapUnitTest { + private static final String TEST_MAP_KEY = "test:redis:map"; + + @Mock + private Jedis jedisMock; + + private RedisMap redisMap; + + @Test + @DisplayName("Size должен выбрасывать IllegalStateException, если реальный размер превышает Integer.MAX_VALUE") + void sizeShouldThrowISE_whenRealSizeExceedsIntegerMaxValue() { + // Arrange + long exceedingSize = (long) Integer.MAX_VALUE + 1; + when(jedisMock.hlen(TEST_MAP_KEY)).thenReturn(exceedingSize); + redisMap = new RedisMap(jedisMock, TEST_MAP_KEY); + + // Act & Assert + assertThatThrownBy(() -> redisMap.size()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("RedisMap size exceeds Integer.MAX_VALUE: " + exceedingSize); + } +}