diff --git a/README.md b/README.md index d6f44cb..d78f50f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,83 @@ -# Практическое задание 1 # CSV Writer - -Мы пишем приложение, которое должно генерировать отчеты в разных форматах и нам хочется генерировать какой-нибудь отчёт в CSV, но проблема в том, что у нас нет такого инструмента. Поэтому мы решаем, что нам нужна библиотека для генерации CSV файлов. -Реализация может быть построена с помощью Reflection и аннотаций. - -Папка model содержит классы, которые нужно сохранять в CSV. Классы можно изменять и добавлять новые. - -**Описание:**
-✅ Написать реализацию Writable, которая будет сохранять объекты в CSV формате в файл
-✅ Написать unit тесты
-✅ Написать JavaDoc
-✅ Для генерации тестовых данных можно использовать Datafaker
-✅ В main должно быть несколько примеров сохранения данных в файл - -**Как выполнить:** -1. Сделать форк этого проекта -2. Сделать ветку dev в своем проекте. Выполнять работу в ней -3. После выполнения работы открыть Pull Request в master -4. Назначить ментора ревьювером -5. Отправить ссылку на PR в тг чат ментору \ No newline at end of file +# CSV Writer + +Кастомная библиотека для генерации CSV файлов из Java объектов. + +## Описание + +Простая и гибкая реализация для экспорта Java объектов в CSV формат. Использует аннотации для конфигурации полей и рефлексию для извлечения данных. + +## Реализованная функциональность + +### Основные компоненты: + +- **`@CsvColumn`** - аннотация для маркировки полей + - `name` - имя колонки в CSV + - `order` - порядок следования колонок + +- **`CsvWriter`** - основной класс для записи в CSV + - Автоматическое создание заголовков + - Сортировка колонок по order + - Экранирование специальных символов + - Поддержка коллекций (объединение через `;`) + - Поддержка enum и null значений + +- **Модели данных** + - `Person` - информация о человеке (имя, фамилия, дата рождения) + - `Student` - студент с оценками + - `Months` - enum для месяцев + +## Использование + +```java +CsvWriter writer = new CsvWriter(); + +// Создаем данные +List people = Arrays.asList( + Person.builder() + .firstName("Иван") + .lastName("Иванов") + .dayOfBirth(15) + .monthOfBirth(Months.MARCH) + .yearOfBirth(1990) + .build() +); + +// Записываем в файл +writer.writeToFile(people, "people.csv"); +``` + +## Покрытие тестами + +**16 юнит и интеграционных тестов**, включая: + +### Базовая функциональность: +- Запись простых объектов и коллекций +- Экранирование специальных символов (`,`, `"`, переносы строк) +- Обработка null значений +- Валидация входных данных +- Работа с enum +- Кастомные имена и порядок колонок + +### Тесты с Datafaker: +- Генерация больших наборов данных (100, 1000 записей) +- Тест производительности + +## Примеры вывода + +### people.csv +```csv +First Name,Last Name,Day,Month,Year +Иван,Иванов,15,MARCH,1990 +Мария,Смирнова,23,JULY,1985 +``` + +### students.csv +```csv +Student Name,Scores +Алиса Иванова,95;88;92;90 +Борис Сидоров,78;85;80;82 +``` + +## Документация + +Все классы и методы содержат JavaDoc с описанием использования. \ No newline at end of file diff --git a/pom.xml b/pom.xml index effc1bd..f821633 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,12 @@ 5.8.1 test + + net.datafaker + datafaker + 2.5.2 + test + \ No newline at end of file diff --git a/src/main/java/org/writer/CsvWriter.java b/src/main/java/org/writer/CsvWriter.java new file mode 100644 index 0000000..9338da5 --- /dev/null +++ b/src/main/java/org/writer/CsvWriter.java @@ -0,0 +1,153 @@ +package org.writer; + +import org.writer.annotation.CsvColumn; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Реализация интерфейса Writable для записи объектов в CSV формат. + * Использует рефлексию для извлечения значений полей из объектов и аннотации для настройки вывода. + */ +public class CsvWriter implements Writable { + + private static final String DELIMITER = ","; + private static final String LINE_SEPARATOR = System.lineSeparator(); + + /** + * Записывает список объектов в CSV файл. + * В выходной файл будут включены только поля, аннотированные @CsvColumn. + * + * @param data список объектов для записи + * @param fileName имя выходного файла + * @throws IllegalArgumentException если data равен null или пустой + * @throws RuntimeException если произошла ошибка при записи файла + */ + @Override + public void writeToFile(List data, String fileName) { + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("Data list cannot be null or empty"); + } + + Class clazz = data.get(0).getClass(); + List fields = getAnnotatedFields(clazz); + + if (fields.isEmpty()) { + throw new IllegalArgumentException("No @CsvColumn annotated fields found in class " + clazz.getName()); + } + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) { + // Записываем заголовок + String header = fields.stream() + .map(FieldInfo::columnName) + .collect(Collectors.joining(DELIMITER)); + writer.write(header); + writer.write(LINE_SEPARATOR); + + // Записываем строки данных + for (Object obj : data) { + String row = fields.stream() + .map(fieldInfo -> getFieldValue(obj, fieldInfo.field())) + .map(this::formatValue) + .collect(Collectors.joining(DELIMITER)); + writer.write(row); + writer.write(LINE_SEPARATOR); + } + } catch (IOException e) { + throw new RuntimeException("Error writing to file: " + fileName, e); + } + } + + /** + * Получает все поля, аннотированные @CsvColumn, из класса. + * Поля сортируются по атрибуту order. + * + * @param clazz класс для анализа + * @return список информации о полях, отсортированный по order + */ + private List getAnnotatedFields(Class clazz) { + List fieldInfos = new ArrayList<>(); + + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(CsvColumn.class)) { + CsvColumn annotation = field.getAnnotation(CsvColumn.class); + String columnName = annotation.name().isEmpty() ? field.getName() : annotation.name(); + fieldInfos.add(new FieldInfo(field, columnName, annotation.order())); + } + } + + // Сортируем по order + fieldInfos.sort(Comparator.comparingInt(FieldInfo::order)); + return fieldInfos; + } + + /** + * Извлекает значение поля из объекта с использованием рефлексии. + * + * @param obj объект, из которого извлекается значение + * @param field поле для извлечения + * @return значение поля, или null если извлечение не удалось + */ + private Object getFieldValue(Object obj, Field field) { + try { + field.setAccessible(true); + return field.get(obj); + } catch (IllegalAccessException e) { + throw new RuntimeException("Error accessing field: " + field.getName(), e); + } + } + + /** + * Форматирует значение для вывода в CSV. + * Обрабатывает null значения, коллекции и специальные символы, требующие экранирования. + * + * @param value значение для форматирования + * @return отформатированное строковое значение + */ + private String formatValue(Object value) { + if (value == null) { + return ""; + } + + // Обрабатываем коллекции + if (value instanceof Collection collection) { + String joined = collection.stream() + .map(Object::toString) + .collect(Collectors.joining(";")); + return escapeValue(joined); + } + + // Обрабатываем enum + if (value instanceof Enum) { + return value.toString(); + } + + return escapeValue(value.toString()); + } + + /** + * Экранирует специальные символы в CSV значениях. + * Оборачивает значения в кавычки, если они содержат запятые, кавычки или переносы строк. + * + * @param value значение для экранирования + * @return экранированное значение + */ + private String escapeValue(String value) { + if (value.contains(",") || value.contains("\"") || value.contains("\n") || value.contains("\r")) { + // Экранируем кавычки путем их удвоения + String escaped = value.replace("\"", "\"\""); + return "\"" + escaped + "\""; + } + return value; + } + + /** + * Внутренний класс для хранения метаданных поля. + */ + private record FieldInfo(Field field, String columnName, int order) { + } +} \ No newline at end of file diff --git a/src/main/java/org/writer/Main.java b/src/main/java/org/writer/Main.java index 40e8213..9d03f32 100644 --- a/src/main/java/org/writer/Main.java +++ b/src/main/java/org/writer/Main.java @@ -1,7 +1,123 @@ package org.writer; +import org.writer.model.Months; +import org.writer.model.Person; +import org.writer.model.Student; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +/** + * Главный класс для демонстрации функциональности CSV Writer. + * Содержит примеры записи различных типов данных в CSV формат. + */ public class Main { + /** + * Точка входа в приложение. + * Демонстрирует три примера использования CsvWriter: + * 1. Запись данных о людях + * 2. Запись данных студентов с оценками + * 3. Запись данных со специальными символами + * + * @param args аргументы командной строки (не используются) + */ public static void main(String[] args) { - System.out.println("Hello world!"); + + // Удаление тестовых файлов в начале выполнения + deleteFileIfExists("people.csv"); + deleteFileIfExists("students.csv"); + deleteFileIfExists("special_cases.csv"); + + CsvWriter writer = new CsvWriter(); + + // Пример 1: Запись объектов Person в CSV + System.out.println("\nПример 1: Записываем данные о людях..."); + List people = Arrays.asList( + Person.builder() + .firstName("Иван") + .lastName("Иванов") + .dayOfBirth(15) + .monthOfBirth(Months.MARCH) + .yearOfBirth(1990) + .build(), + Person.builder() + .firstName("Мария") + .lastName("Смирнова") + .dayOfBirth(23) + .monthOfBirth(Months.JULY) + .yearOfBirth(1985) + .build(), + Person.builder() + .firstName("Петр") + .lastName("Петров") + .dayOfBirth(8) + .monthOfBirth(Months.DECEMBER) + .yearOfBirth(1992) + .build() + ); + + writer.writeToFile(people, "people.csv"); + System.out.println("Данные о людях записаны в файл people.csv"); + + // Пример 2: Запись объектов Student с коллекциями в CSV + System.out.println("\nПример 2: Записываем данные студентов с оценками..."); + List students = Arrays.asList( + Student.builder() + .name("Алиса Иванова") + .score(Arrays.asList("95", "88", "92", "90")) + .build(), + Student.builder() + .name("Борис Сидоров") + .score(Arrays.asList("78", "85", "80", "82")) + .build(), + Student.builder() + .name("Вера Кузнецова") + .score(Arrays.asList("92", "94", "89", "96")) + .build() + ); + + writer.writeToFile(students, "students.csv"); + System.out.println("Данные студентов записаны в файл students.csv"); + + // Пример 3: Запись данных со специальными символами + System.out.println("\nПример 3: Записываем данные со специальными символами..."); + List specialCases = Arrays.asList( + Person.builder() + .firstName("Дэвид") + .lastName("О'Лири") + .dayOfBirth(2) + .monthOfBirth(Months.MAY) + .yearOfBirth(1958) + .build(), + Person.builder() + .firstName("Габриэль") + .lastName("Гарсиа-Маркес") + .dayOfBirth(6) + .monthOfBirth(Months.MARCH) + .yearOfBirth(1927) + .build() + ); + + writer.writeToFile(specialCases, "special_cases.csv"); + System.out.println("Специальные случаи записаны в файл special_cases.csv"); + + System.out.println("\nВсе примеры успешно выполнены!"); + } + + /** + * Удаляет файл, если он существует. + * + * @param filename имя файла для удаления + */ + private static void deleteFileIfExists(String filename) { + File file = new File(filename); + if (file.exists()) { + if (file.delete()) { + System.out.println("Удален существующий файл: " + filename); + } else { + System.out.println("Не удалось удалить файл: " + filename); + } + } } } \ No newline at end of file diff --git a/src/main/java/org/writer/annotation/CsvColumn.java b/src/main/java/org/writer/annotation/CsvColumn.java new file mode 100644 index 0000000..e58c1c0 --- /dev/null +++ b/src/main/java/org/writer/annotation/CsvColumn.java @@ -0,0 +1,29 @@ +package org.writer.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Аннотация для указания полей, которые необходимо сохранить в CSV-файл. + * Поля без аннотаций не будут сохранены. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CsvColumn { + + /** + * Название колонки в CSV-файле. Если не указано, будет использовано название поля. + * + * @return имя колонки + */ + String name() default ""; + + /** + * Порядковый номер колонки в CSV-файле, колонки располагаются слева-направо по возрастанию номера. + * + * @return порядковый номер колонки + */ + int order() default 0; +} diff --git a/src/main/java/org/writer/model/Person.java b/src/main/java/org/writer/model/Person.java index ba8576b..802da94 100644 --- a/src/main/java/org/writer/model/Person.java +++ b/src/main/java/org/writer/model/Person.java @@ -3,20 +3,45 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.writer.annotation.CsvColumn; +/** + * Модель данных для представления человека. + * Содержит информацию об имени, фамилии и дате рождения. + */ @Data @Builder @AllArgsConstructor public class Person { + /** + * Имя человека. + */ + @CsvColumn(name = "First Name", order = 1) private String firstName; + /** + * Фамилия человека. + */ + @CsvColumn(name = "Last Name", order = 2) private String lastName; + /** + * День рождения (1-31). + */ + @CsvColumn(name = "Day", order = 3) private int dayOfBirth; + /** + * Месяц рождения. + */ + @CsvColumn(name = "Month", order = 4) private Months monthOfBirth; + /** + * Год рождения. + */ + @CsvColumn(name = "Year", order = 5) private int yearOfBirth; } diff --git a/src/main/java/org/writer/model/Student.java b/src/main/java/org/writer/model/Student.java index 2b549c4..4d47b07 100644 --- a/src/main/java/org/writer/model/Student.java +++ b/src/main/java/org/writer/model/Student.java @@ -3,15 +3,31 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import org.writer.annotation.CsvColumn; import java.util.List; +/** + * Модель данных для представления студента. + * Содержит имя студента и список его оценок. + */ @Data @Builder @AllArgsConstructor public class Student { + /** + * Имя студента. + */ + @CsvColumn(name = "Student Name", order = 1) private String name; + /** + * Список оценок студента. + * При записи в CSV оценки будут объединены через точку с запятой (;). + * + *

Пример: если оценки ["95", "88", "92"], то в CSV будет записано "95;88;92" + */ + @CsvColumn(name = "Scores", order = 2) private List score; } \ No newline at end of file diff --git a/src/test/java/org/writer/CsvWriterTest.java b/src/test/java/org/writer/CsvWriterTest.java new file mode 100644 index 0000000..855c33a --- /dev/null +++ b/src/test/java/org/writer/CsvWriterTest.java @@ -0,0 +1,510 @@ +package org.writer; + +import net.datafaker.Faker; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.writer.model.Months; +import org.writer.model.Person; +import org.writer.model.Student; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Включает базовые тесты функциональности CsvWriter и тесты с генерацией данных через Datafaker. + */ +class CsvWriterTest { + + private CsvWriter csvWriter; + private Faker faker; + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + csvWriter = new CsvWriter(); + faker = new Faker(); + } + + // ============================================ + // Базовые тесты функциональности + // ============================================ + + /** + * Проверяет запись простых объектов Person в CSV файл и корректность заголовков и данных. + */ + @Test + void testWriteSimplePersonData() throws IOException { + List people = Arrays.asList( + Person.builder() + .firstName("John") + .lastName("Doe") + .dayOfBirth(15) + .monthOfBirth(Months.MARCH) + .yearOfBirth(1990) + .build(), + Person.builder() + .firstName("Jane") + .lastName("Smith") + .dayOfBirth(23) + .monthOfBirth(Months.JULY) + .yearOfBirth(1985) + .build() + ); + + Path outputFile = tempDir.resolve("people.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + assertTrue(Files.exists(outputFile)); + List lines = Files.readAllLines(outputFile); + assertEquals(3, lines.size()); // Заголовок + 2 строки данных + + // Проверяем заголовок + assertEquals("First Name,Last Name,Day,Month,Year", lines.get(0)); + + // Проверяем строки данных + assertEquals("John,Doe,15,MARCH,1990", lines.get(1)); + assertEquals("Jane,Smith,23,JULY,1985", lines.get(2)); + } + + /** + * Проверяет запись объектов Student с коллекциями. + * Коллекции должны объединяться через точку с запятой. + */ + @Test + void testWriteStudentDataWithCollections() throws IOException { + List students = Arrays.asList( + Student.builder() + .name("Alice") + .score(Arrays.asList("95", "88", "92")) + .build(), + Student.builder() + .name("Bob") + .score(Arrays.asList("78", "85", "80")) + .build() + ); + + Path outputFile = tempDir.resolve("students.csv"); + + csvWriter.writeToFile(students, outputFile.toString()); + + assertTrue(Files.exists(outputFile)); + List lines = Files.readAllLines(outputFile); + assertEquals(3, lines.size()); + + // Проверяем заголовок + assertEquals("Student Name,Scores", lines.get(0)); + + // Проверяем данные - коллекции должны быть объединены через точку с запятой + assertEquals("Alice,95;88;92", lines.get(1)); + assertEquals("Bob,78;85;80", lines.get(2)); + } + + /** + * Проверяет экранирование специальных символов. + * Значения с запятыми должны оборачиваться в кавычки. + */ + @Test + void testEscapeSpecialCharacters() throws IOException { + List people = Collections.singletonList( + Person.builder() + .firstName("O'Brien") + .lastName("Smith, Jr.") + .dayOfBirth(1) + .monthOfBirth(Months.JANUARY) + .yearOfBirth(1990) + .build() + ); + + Path outputFile = tempDir.resolve("special.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + List lines = Files.readAllLines(outputFile); + assertEquals(2, lines.size()); + + // Фамилия содержит запятую, должна быть в кавычках + assertTrue(lines.get(1).contains("\"Smith, Jr.\"")); + } + + /** + * Проверяет преобразование null значений в пустые строки. + */ + @Test + void testHandleNullValues() throws IOException { + List people = Collections.singletonList( + Person.builder() + .firstName("John") + .lastName(null) // null значение + .dayOfBirth(15) + .monthOfBirth(null) // null enum + .yearOfBirth(1990) + .build() + ); + + Path outputFile = tempDir.resolve("nulls.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + List lines = Files.readAllLines(outputFile); + assertEquals(2, lines.size()); + + // Null значения должны быть представлены пустыми строки + String dataRow = lines.get(1); + String[] values = dataRow.split(",", -1); // -1, чтобы сохранить пустые строки + assertEquals("John", values[0]); + assertEquals("", values[1]); // null lastName + assertEquals("15", values[2]); + assertEquals("", values[3]); // null month + assertEquals("1990", values[4]); + } + + /** + * Проверяет, что пустой список данных вызывает исключение. + */ + @Test + void testEmptyDataListThrowsException() { + List emptyList = Collections.emptyList(); + Path outputFile = tempDir.resolve("empty.csv"); + + assertThrows(IllegalArgumentException.class, () -> csvWriter.writeToFile(emptyList, outputFile.toString())); + } + + /** + * Проверяет, что null список данных вызывает исключение. + */ + @Test + void testNullDataListThrowsException() { + Path outputFile = tempDir.resolve("null.csv"); + + assertThrows(IllegalArgumentException.class, () -> csvWriter.writeToFile(null, outputFile.toString())); + } + + /** + * Проверяет, что порядок полей соблюдается согласно атрибуту order в аннотации. + */ + @Test + void testFieldOrderingIsRespected() throws IOException { + List people = Collections.singletonList( + Person.builder() + .firstName("John") + .lastName("Doe") + .dayOfBirth(15) + .monthOfBirth(Months.MARCH) + .yearOfBirth(1990) + .build() + ); + + Path outputFile = tempDir.resolve("order.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + List lines = Files.readAllLines(outputFile); + String header = lines.get(0); + + // Поля должны быть в порядке, указанном в @CsvColumn(order=...) + // Person: firstName(1), lastName(2), dayOfBirth(3), monthOfBirth(4), yearOfBirth(5) + assertEquals("First Name,Last Name,Day,Month,Year", header); + } + + /** + * Проверяет использование кастомных имен колонок из аннотации. + */ + @Test + void testCustomColumnNames() throws IOException { + List people = Collections.singletonList( + Person.builder() + .firstName("John") + .lastName("Doe") + .dayOfBirth(15) + .monthOfBirth(Months.MARCH) + .yearOfBirth(1990) + .build() + ); + + Path outputFile = tempDir.resolve("custom.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + List lines = Files.readAllLines(outputFile); + + // Проверяем, что используются кастомные имена из @CsvColumn(name=...) + assertTrue(lines.get(0).contains("First Name")); + assertTrue(lines.get(0).contains("Last Name")); + } + + /** + * Проверяет обработку пустых коллекций в полях. + */ + @Test + void testEmptyCollectionInField() throws IOException { + List students = Collections.singletonList( + Student.builder() + .name("Alice") + .score(Collections.emptyList()) + .build() + ); + + Path outputFile = tempDir.resolve("empty_collection.csv"); + + csvWriter.writeToFile(students, outputFile.toString()); + + List lines = Files.readAllLines(outputFile); + assertEquals(2, lines.size()); + assertEquals("Alice,", lines.get(1)); // Пустая коллекция превращается в пустую строку + } + + /** + * Проверяет работу с различными значениями enum. + */ + @Test + void testMultipleEnumValues() throws IOException { + List people = Arrays.asList( + Person.builder() + .firstName("January") + .lastName("Person") + .dayOfBirth(1) + .monthOfBirth(Months.JANUARY) + .yearOfBirth(2000) + .build(), + Person.builder() + .firstName("December") + .lastName("Person") + .dayOfBirth(31) + .monthOfBirth(Months.DECEMBER) + .yearOfBirth(2000) + .build() + ); + + Path outputFile = tempDir.resolve("enums.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + List lines = Files.readAllLines(outputFile); + assertTrue(lines.get(1).contains("JANUARY")); + assertTrue(lines.get(2).contains("DECEMBER")); + } + + /** + * Проверяет экранирование кавычек в данных. + * Кавычки должны удваиваться и значение должно оборачиваться в кавычки. + */ + @Test + void testQuotesInData() throws IOException { + List people = Collections.singletonList( + Person.builder() + .firstName("John \"Johnny\"") + .lastName("Doe") + .dayOfBirth(1) + .monthOfBirth(Months.JANUARY) + .yearOfBirth(1990) + .build() + ); + + Path outputFile = tempDir.resolve("quotes.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + List lines = Files.readAllLines(outputFile); + String dataRow = lines.get(1); + + // Кавычки должны быть экранированы удвоением и обернуты в кавычки + assertTrue(dataRow.contains("\"John \"\"Johnny\"\"\"")); + } + + // ============================================ + // Тесты с использованием Datafaker + // ============================================ + + /** + * Проверяет запись большого набора данных (100 записей) сгенерированных через Datafaker. + */ + @Test + void testLargeDataSetWithDatafaker() throws IOException { + // Генерируем 100 случайных людей используя Datafaker + List people = IntStream.range(0, 100) + .mapToObj(i -> createPersonWithFaker(faker)) + .toList(); + + Path outputFile = tempDir.resolve("large_dataset.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + assertTrue(Files.exists(outputFile)); + List lines = Files.readAllLines(outputFile); + assertEquals(101, lines.size()); // Заголовок + 100 строк данных + + // Проверяем наличие заголовка + assertEquals("First Name,Last Name,Day,Month,Year", lines.get(0)); + + // Проверяем, что все строки имеют правильное количество колонок + for (int i = 1; i < lines.size(); i++) { + String[] columns = lines.get(i).split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1); + assertEquals(5, columns.length, "Строка " + i + " должна иметь 5 колонок"); + } + } + + /** + * Проверяет генерацию студентов с оценками через Datafaker. + */ + @Test + void testDatafakerStudents() throws IOException { + List students = IntStream.range(0, 50) + .mapToObj(i -> Student.builder() + .name(faker.name().fullName()) + .score(IntStream.range(0, 4) + .mapToObj(j -> String.valueOf(faker.number().numberBetween(60, 100))) + .toList()) + .build()) + .toList(); + + Path outputFile = tempDir.resolve("students_datafaker.csv"); + + csvWriter.writeToFile(students, outputFile.toString()); + + assertTrue(Files.exists(outputFile)); + List lines = Files.readAllLines(outputFile); + assertEquals(51, lines.size()); // Заголовок + 50 студентов + + // Проверяем, что оценки правильно форматированы (разделены точкой с запятой) + for (int i = 1; i < lines.size(); i++) { + String line = lines.get(i); + assertTrue(line.contains(";") || line.matches(".*,\\d+$"), + "Строка должна содержать оценки, разделенные точкой с запятой"); + } + } + + /** + * Проверяет генерацию реалистичных персональных данных. + */ + @Test + void testGenerateRealisticPersonData() throws IOException { + // Генерируем реалистичные данные людей + List people = IntStream.range(0, 20) + .mapToObj(i -> createPersonWithFaker(faker)) + .toList(); + + Path outputFile = tempDir.resolve("realistic_people.csv"); + + csvWriter.writeToFile(people, outputFile.toString()); + + assertTrue(Files.exists(outputFile)); + List lines = Files.readAllLines(outputFile); + assertEquals(21, lines.size()); + + // Проверяем структуру реалистичных данных + for (int i = 1; i < lines.size(); i++) { + String[] parts = lines.get(i).split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"); + assertNotNull(parts[0]); // firstName + assertNotNull(parts[1]); // lastName + assertTrue(Integer.parseInt(parts[2]) >= 1 && Integer.parseInt(parts[2]) <= 28); // day + assertTrue(parts[3].matches("[A-Z]+")); // month enum + assertTrue(Integer.parseInt(parts[4]) >= 1970 && Integer.parseInt(parts[4]) <= 2000); // year + } + } + + /** + * Проверяет генерацию студентов со случайными оценками. + */ + @Test + void testGenerateStudentsWithRandomScores() throws IOException { + // Генерируем студентов со случайными тестовыми оценками + List students = IntStream.range(0, 30) + .mapToObj(i -> { + // Генерируем 3-5 оценок на студента + int numScores = faker.number().numberBetween(3, 6); + List scores = IntStream.range(0, numScores) + .mapToObj(j -> String.valueOf(faker.number().numberBetween(50, 100))) + .toList(); + + return Student.builder() + .name(faker.name().fullName()) + .score(scores) + .build(); + }) + .toList(); + + Path outputFile = tempDir.resolve("students_random_scores.csv"); + + csvWriter.writeToFile(students, outputFile.toString()); + + assertTrue(Files.exists(outputFile)); + List lines = Files.readAllLines(outputFile); + assertEquals(31, lines.size()); + + // Проверяем формат оценок + for (int i = 1; i < lines.size(); i++) { + String line = lines.get(i); + String[] parts = line.split(",", 2); + String scoresStr = parts[1]; + + // Оценки должны быть разделены точкой с запятой + String[] scores = scoresStr.split(";"); + assertTrue(scores.length >= 3 && scores.length <= 5); + + for (String score : scores) { + int scoreValue = Integer.parseInt(score.trim()); + assertTrue(scoreValue >= 50 && scoreValue <= 100); + } + } + } + + /** + * Тест производительности с большим набором данных (1000 записей). + */ + @Test + void testPerformanceWithLargeDataset() throws IOException { + // Большой набор данных (1000 записей) + long startTime = System.currentTimeMillis(); + + List largeDataset = IntStream.range(0, 1000) + .mapToObj(i -> createPersonWithFaker(faker)) + .toList(); + + Path outputFile = tempDir.resolve("large_performance_test.csv"); + + csvWriter.writeToFile(largeDataset, outputFile.toString()); + + // Отсчитываем время выполнения записи + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + assertTrue(Files.exists(outputFile)); + List lines = Files.readAllLines(outputFile); + assertEquals(1001, lines.size()); // Заголовок + 1000 записей + + // Проверка производительности - должно завершиться за разумное время (< 5 секунд) + assertTrue(duration < 5000, "Запись 1000 записей должна занимать менее 5 секунд, заняло: " + duration + "мс"); + + System.out.println("Тест производительности: 1000 записей записано за " + duration + "мс"); + } + + /** + * Вспомогательный метод для создания Person используя конкретный экземпляр Faker. + * + * @param faker экземпляр Faker + * @return сгенерированный объект Person + */ + private Person createPersonWithFaker(Faker faker) { + return Person.builder() + .firstName(faker.name().firstName()) + .lastName(faker.name().lastName()) + .dayOfBirth(faker.number().numberBetween(1, 28)) + .monthOfBirth(Months.values()[faker.number().numberBetween(0, 12)]) + .yearOfBirth(faker.number().numberBetween(1970, 2000)) + .build(); + } +} \ No newline at end of file