2013-11-26 4 views
5

Мне было интересно, если кто-нибудь найдет способ заглушить/издеваться над логикой внутри лямбда, не делая видимости лямбды?Unit Testing for Java 8 Lambdas

public List<Item> processFile(String fileName) { 
    // do some magic.. 
    Function<String, List<String>> reader = (fileName) -> { 
     List<String> items = new ArrayList<>(); 
     try (BufferedReader br = new BufferedReader(new FileReader(fileName))) { 
      String output; 
      while ((output = br.readLine()) != null) { 
      items.add(output); 
      } 
     } catch (IOException e) { 
      e.printStackTrace(); 
     } 
    return items; 
    }; 

    List<String> lines = reader.apply("file.csv"); 
    // do some more magic.. 
} 
+0

lambda может загружать контекст, как и все остальное (если вы хотите его заглушить). но будьте осторожны, что лямбда без захвата переменных, скорее всего, будет синглом. – aepurniet

ответ

9

Я хотел бы сказать, правило таково, что если лямбда-выражение является настолько сложным, что вы чувствуете необходимость издеваться битами этого, что это, вероятно, слишком сложно. Его следует разбить на более мелкие куски, которые составлены вместе, или, возможно, модель должна быть скорректирована, чтобы сделать ее более подходящей для композиции.

Я скажу, что Andrey Chaschev's answer, который предлагает параметрирование зависимости, является хорошим и, вероятно, применим в некоторых ситуациях. Итак, +1 для этого. Один мог продолжать этот процесс и разбить обработку на более мелкие куски, например, так:

public List<Item> processFile(
    String fileName, 
    Function<String, BufferedReader> toReader, 
    Function<BufferedReader, List<String>> toStringList, 
    Function<List<String>, List<Item>> toItemList) 
{ 
    List<String> lines = null; 
    try (BufferedReader br = toReader.apply(fileName)) { 
     lines = toStringList.apply(br); 
    } catch (IOException ioe) { /* ... */ } 

    return toItemList.apply(lines); 
} 

пару замечаний по этому вопросу, хотя. Во-первых, это не работает так, как написано, так как различные lambdas бросают pesky IOExceptions, которые проверяются, и тип Function не объявлен, чтобы выбросить это исключение. Во-вторых, лямбды, которые вы должны передать этой функции, чудовищны. Несмотря на то, что это не сработало (из-за отмеченных исключений), я написал:

void processAnActualFile() { 
    List<Item> items = processFile(
     "file.csv", 
     fname -> new BufferedReader(new FileReader(fname)), 
       // ERROR: uncaught IOException 
     br -> { 
      List<String> result = new ArrayList<>(); 
      String line; 
      while ((line = br.readLine()) != null) { 
       result.add(line); 
      } 
      return result; 
     },  // ERROR: uncaught IOException 
     stringList -> { 
      List<Item> result = new ArrayList<>(); 
      for (String line : stringList) { 
       result.add(new Item(line)); 
      } 
      return result; 
     }); 
} 

Ugh! Я думаю, что обнаружил новый запах кода:

Если вам нужно написать цикл for или loop внутри лямбда, вы делаете что-то неправильно.

Несколько вещей здесь. Во-первых, библиотека ввода-вывода действительно состоит из разных частей реализации (InputStream, Reader, BufferedReader), которые плотно связаны. На самом деле не полезно пытаться разбить их. В самом деле, библиотека развилась так, что есть некоторые удобные утилиты (например, NIO Files.readAllLines), которые обрабатывают кучу работы для вас.

Более важным моментом является то, что проектирование функций, которые передают совокупности (списки) значений между собой и составляющих эти функции, на самом деле является неправильным способом. Это приводит к тому, что каждая функция должна писать петлю внутри нее. Мы действительно хотим написать функции записи, каждая из которых работает с одним значением, а затем пусть новая библиотека Streams в Java 8 позаботится об агрегировании для нас.

Ключевая функция для извлечения здесь из кода, описанного в комментарии «сделать еще несколько магии», которая преобразует List<String> в List<Item>. Мы хотим, чтобы извлечь вычисление, которое преобразует одинString в Item, как это:

class Item { 
    static Item fromString(String s) { 
     // do a little bit of magic 
    } 
} 

После того как вы это, то вы можете позволить Ручьи и NIO библиотеки делать кучу работы для вас:

public List<Item> processFile(String fileName) { 
    try (Stream<String> lines = Files.lines(Paths.get(fileName))) { 
     return lines.map(Item::fromString) 
        .collect(Collectors.toList()); 
    } catch (IOException ioe) { 
     ioe.printStackTrace(); 
     return Collections.emptyList(); 
    } 
} 

(Обратите внимание, что более половины этого короткого метода предназначено для работы с IOException.)

Теперь, если вы хотите выполнить некоторые модульные испытания, то, что вам действительно нужно проверить, - это немного магии. Таким образом, вы заверните его в другой поток трубопровода, как это:

void testItemCreation() { 
    List<Item> result = 
     Arrays.asList("first", "second", "third") 
       .stream() 
       .map(Item::fromString) 
       .collect(Collectors.toList()); 
    // make assertions over result 
} 

(На самом деле, даже это не совсем верно Вы хотели бы писать тесты для преобразования одну строку в одну Item Но.. может быть, у вас есть какие-то тестовые данные где-то, так что вы хотите преобразовать его в список элементов этого пути, а затем сделать глобальные утверждения по взаимосвязи результирующих элементов в списке.)


Я бродила довольно далеко от вашего первоначального вопроса о том, как разбить лямбду. Пожалуйста, прости меня за то, что я потворствую себе.

Лямбда в оригинальном примере довольно неудачная, так как библиотеки ввода-вывода Java довольно громоздки, и в библиотеке NIO есть новые API-интерфейсы, которые превращают этот пример в однострочный.

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

3

Я не уверен, если это то, что вы просите, но вы могли бы извлечь из лямбда лямбда, т.е. к другому классу или как и передать его в качестве параметра. В примере ниже я издеваться создание читателя:

public static void processFile(String fileName, Function<String, BufferedReader> readerSupplier) { 
    // do some magic.. 
    Function<String, List<String>> reader = (name) -> { 
     List<String> items = new ArrayList<>(); 
     try(BufferedReader br = readerSupplier.apply(name)){ 
      String output; 
      while ((output = br.readLine()) != null) { 
       items.add(output); 
      } 
     } catch (IOException e) { 
      e.printStackTrace(); 
     } 

     return items; 
    }; 

    List<String> lines = reader.apply(fileName); 
    // do some more magic.. 
} 

public static void main(String[] args) { 
    // mocked call 
    processFile("file.csv", name -> new BufferedReader(new StringReader("line1\nline2\n"))); 

    //original call 
    processFile("1.csv", name -> { 
     try { 
      return new BufferedReader(new FileReader(name)); 
     } catch (FileNotFoundException e) { 
      throw new RuntimeException(e); 
     } 
    }); 
} 
+0

+1 для потенциально полезной методики передачи функции в качестве параметра. –