2013-05-31

Тестирование файловой системы

Симуляция реального мира в песочнице
Май выдался богатым на новую и полезную информацию по тестированию: цикл статей от Сергея Теплякова, интересное обсуждение в Радио-Т.

В это же время появилась интересная задача связанная с доступом к файловой системе. Алгоритм:
1. получаем список файлов
2. каким-то образом фильтруем
3. обрабатываем отфильтрованные файлы.

Работа с ФС (по крайней мере в 6-й java) сосредоточена в классе File: list, delete, работа с путями. По сути эти методы относятся к сервису. Хочется иметь интерфейс чтобы убрать зависимость и получить возможность для подмены реализации, ну или хотя бы подмены mock-ами для тестирования.

Конечно можно выйти из положения, создавая нужную иерархию в определенном месте файловой системы и работать с настоящим доступом, но что, если хочется просто протестировать алгоритм.

Постановка задачи

По заданному пути
удалить пять файлов
с учетом сортировки по возрастанию
имя которых начинается с цифр
содержащие первой строкой ШИББОЛЕТ.

Решение

Реализация находится здесь, тесты здесь.

Сервис

FileService представляет собой фактически обертку с некоторыми удобными методами

public class FileService {
    /**
     * Get files in path
     */
    public Collection dir(File path) {
        return Arrays.asList(path.listFiles());
    }

    /**
     * Delete path
     */
    public void delete(File file) {
        if (!file.delete()) {
            log.debug("Cannot delete " + file.getPath());
        }
    }

    /**
     * Open file
     */
    public InputStream openStream(File path) throws FileNotFoundException {
        return new FileInputStream(path);
    }

    public Reader openReader(File path) throws FileNotFoundException {
        return new FileReader(path);
    }

    public boolean isDir(File path) {
        return path.isDirectory();
    }
}

Фильтрация

Фильтрация осуществляется при помощи удобнейшего FluentIterable:

public static Iterable filter(FileService fileService, File dir, int count, String mask, String prefix) {
        return FluentIterable
                .from(Ordering.natural().immutableSortedCopy(fileService.dir(dir)))
                .filter(new NamePredicate(mask))
                .filter(new ContentPredicate(fileService, prefix))
                .limit(count)
                .toImmutableList();
    } 

И пары предикатов для фильтрации по имени и содержимому. В предикат фильтрации по содержимому передается сервис файловой системы, плюс в Java 7 можно использовать очень удобный try для работы с Closeable ресурсами:
public class ContentPredicate implements Predicate {
    private FileService fileService;
    private String prefix;

    public ContentPredicate(FileService fileService, String prefix) {
        this.fileService = fileService;
        this.prefix = prefix;
    }

    @Override
    public boolean apply(@Nullable File file) {
        try (BufferedReader br = new BufferedReader(fileService.openReader(file))) {
            return prefix.equals(br.readLine());
        } catch (IOException ioe) {
            ContentPredicate.log.debug("Cannot read file " + file);
        }
        return false;
    }
}

Тестирование

С помощью mockito подменяем возвращаемые значения при тестировании предикатов и фильтра. Сами тесты получаются очень простыми и быстрыми:

@Test
    public void FilterTest() throws FileNotFoundException {
        FileService fileService = Mockito.mock(FileService.class);
        File dir = new File("/test/dir");
        List files = Arrays.asList(
                new File(dir, "100file.name"), // 0
                new File(dir, "bl"), // 1
                new File(dir, "099file.name"), // 2
                new File(dir, "bla"), // 3
                new File(dir, "098file.name"), // 4
                new File(dir, "blah"), // 5
                new File(dir, "097file.name")); // 6
        Mockito.when(fileService.dir(dir)).thenReturn(files);
        Mockito.when(fileService.openReader(files.get(6))).thenReturn(new StringReader("TEST"));
        Mockito.when(fileService.openReader(files.get(4))).thenReturn(new StringReader("TEST"));
        Mockito.when(fileService.openReader(files.get(2))).thenReturn(new StringReader("!!!!"));
        Mockito.when(fileService.openReader(files.get(0))).thenReturn(new StringReader("TEST"));

        Iterable result = Application.filter(fileService, dir, 3, "^\\d+.*$", "TEST");
        Iterator i = result.iterator();
        Assert.assertEquals(files.get(6), i.next());
        Assert.assertEquals(files.get(4), i.next());
        Assert.assertEquals(files.get(0), i.next());
        Assert.assertFalse(i.hasNext());
    }

Итог

Из минусов: имеется обертка, которая нужна лишь для тестирования "DIP головного мозга". Плюсы: классы становятся простыми и легко модифицируемым, самая важную логику становится легко тестировать.