Статьи

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

Тестировать и отлаживать многопоточные программы очень сложно. Теперь возьмите одни и те же программы и массово распределите их по нескольким JVM, развернутым на кластере машин, и сложность сойдет на нет. Один из способов преодоления этой сложности состоит в том, чтобы проводить тестирование изолированно и выявлять как можно больше ошибок локально MRUnit  — это среда тестирования, которая позволяет вам тестировать и отлаживать задания Map Reduce изолированно, не раскручивая кластер Hadoop. В этом посте мы рассмотрим различные функции MRUnit, пройдя простую работу с MapReduce.

Допустим, мы хотим взять входные данные ниже и создать инвертированный индекс, используя  MapReduce .

вход

www.kohls.com,clothes,shoes,beauty,toys
www.amazon.com,books,music,toys,ebooks,movies,computers
www.ebay.com,auctions,cars,computers,books,antiques
www.macys.com,shoes,clothes,toys,jeans,sweaters
www.kroger.com,groceries

Ожидаемый результат

antiques      www.ebay.com
auctions      www.ebay.com
beauty        www.kohls.com
books         www.ebay.com,www.amazon.com
cars          www.ebay.com
clothes       www.kohls.com,www.macys.com
computers     www.amazon.com,www.ebay.com
ebooks        www.amazon.com
jeans         www.macys.com
movies        www.amazon.com
music         www.amazon.com
shoes         www.kohls.com,www.macys.com
sweaters      www.macys.com
toys          www.macys.com,www.amazon.com,www.kohls.com
groceries     www.kroger.com

ниже Mapper и Reducer, которые делают преобразование

public class InvertedIndexMapper extends MapReduceBase implements Mapper<LongWritable, Text, Text, Text> {
public static final int RETAIlER_INDEX = 0;
 
 @Override
 public void map(LongWritable longWritable, Text text, OutputCollector<Text, Text> outputCollector, Reporter reporter) throws IOException {
  final String[] record = StringUtils.split(text.toString(), ",");
  final String retailer = record[RETAIlER_INDEX];
  for (int i = 1; i < record.length; i++) {
   final String keyword = record[i];
   outputCollector.collect(new Text(keyword), new Text(retailer));
   }
  }
 }
public class InvertedIndexReducer extends MapReduceBase implements Reducer<Text, Text, Text, Text> {
@Override
 public void reduce(Text text, Iterator<Text> textIterator, OutputCollector<Text, Text> outputCollector, Reporter reporter) throws IOException {
  final String retailers = StringUtils.join(textIterator, ',');
  outputCollector.collect(text, new Text(retailers));
  }
 }

Детали реализации не очень важны, но в основном Mapper получает строку за раз, разбивает строку и выдает пары ключ-значение, где Key — это категория продукта, а value — это веб-сайт, который продает продукт. Например, линия  розничной торговли, категория 1, категория 2  будет отправлена как ( категория 1, продавец)  и (категория a2, продавец) . Редуктор получает ключ и список значений, преобразует список значений в строку с разделителями-запятыми и выдает ключ и значение.

Теперь давайте использовать MRUnit для написания различных тестов для этой работы. Три основных класса в MRUnits:  MapDriver  для тестирования Mapper,  ReduceDriver  для тестирования Reducer и  MapReduceDriver  для сквозного тестирования MapReduce Job. Так мы настроим тестовый класс.

public class InvertedIndexJobTest {

 private MapDriver<LongWritable, Text, Text, Text> mapDriver;
 private ReduceDriver<Text, Text, Text, Text> reduceDriver;
 private MapReduceDriver<LongWritable, Text, Text, Text, Text, Text> mapReduceDriver;

 @Before
 public void setUp() throws Exception {

 final InvertedIndexMapper mapper = new InvertedIndexMapper();
 final InvertedIndexReducer reducer = new InvertedIndexReducer();

 mapDriver = MapDriver.newMapDriver(mapper);
 reduceDriver = ReduceDriver.newReduceDriver(reducer);
 mapReduceDriver = MapReduceDriver.newMapReduceDriver(mapper, reducer);
 }
}

MRUnit поддерживает два стиля тестирования. Первый стиль состоит в том, чтобы сообщать фреймворку как входные, так и выходные значения, и позволить фреймворку делать утверждения, второй — это более традиционный подход, когда вы делаете утверждение самостоятельно. Давайте напишем тест, используя первый подход.

@Test
 public void testMapperWithSingleKeyAndValue() throws Exception {
 final LongWritable inputKey = new LongWritable(0);
 final Text inputValue = new Text("www.kroger.com,groceries");
 
 final Text outputKey = new Text("groceries");
 final Text outputValue = new Text("www.kroger.com");

 mapDriver.withInput(inputKey, inputValue);
 mapDriver.withOutput(outputKey, outputValue);
 mapDriver.runTest();

 }

В тесте выше мы сообщаем фреймворку обоим парам ключ и значение ввода и вывода, и фреймворк делает это за нас. Этот тест может быть написан более традиционным способом, как следует

@Test
 public void testMapperWithSingleKeyAndValueWithAssertion() throws Exception {
 final LongWritable inputKey = new LongWritable(0);
 final Text inputValue = new Text("www.kroger.com,groceries");
 final Text outputKey = new Text("groceries");
 final Text outputValue = new Text("www.kroger.com");

 mapDriver.withInput(inputKey, inputValue);
 final List<Pair<Text, Text>> result = mapDriver.run();

 assertThat(result)
 .isNotNull()
 .hasSize(1)
 .containsExactly(new Pair<Text, Text>(outputKey, outputValue));
}

Иногда Mapper генерирует несколько пар Key Value для одного ввода. MRUnit предоставляет свободный API для поддержки этого варианта использования. Вот пример

@Test
 public void testMapperWithSingleInputAndMultipleOutput() throws Exception {
 final LongWritable key = new LongWritable(0);
mapDriver.withInput(key, new Text("www.amazon.com,books,music,toys,ebooks,movies,computers"));
 final List<Pair<Text, Text>> result = mapDriver.run();
 
 final Pair<Text, Text> books = new Pair<Text, Text>(new Text("books"), new Text("www.amazon.com"));
 final Pair<Text, Text> toys = new Pair<Text, Text>(new Text("toys"), new Text("www.amazon.com"));

assertThat(result)
 .isNotNull()
 .hasSize(6)
 .contains(books, toys);
}

Вы пишете тест на снижение точно так же.

@Test
 public void testReducer() throws Exception {
final Text inputKey = new Text("books");
final ImmutableList<Text> inputValue = ImmutableList.of(new Text("www.amazon.com"), new Text("www.ebay.com"));

reduceDriver.withInput(inputKey,inputValue);
final List<Pair<Text, Text>> result = reduceDriver.run();
final Pair<Text, Text> pair2 = new Pair<Text, Text>(inputKey, new Text("www.amazon.com,www.ebay.com"));

 assertThat(result)
 .isNotNull()
 .hasSize(1)
 .containsExactly(pair2);
 }

Наконец, вы можете использовать MapReduceDriver, чтобы протестировать ваши Mapper, Combiner и Reducer вместе как одно задание. Вы также можете передать несколько пар ключ-значение в качестве входных данных для вашей работы Тест ниже демонстрирует MapReduceDriver в действии

@Test
 public void testMapReduce() throws Exception {
 mapReduceDriver.withInput(new LongWritable(0), new Text("www.kohls.com,clothes,shoes,beauty,toys"));
 mapReduceDriver.withInput(new LongWritable(1), new Text("www.macys.com,shoes,clothes,toys,jeans,sweaters"));

final List<Pair<Text, Text>> result = mapReduceDriver.run();

final Pair clothes = new Pair<Text, Text>(new Text("clothes"), new Text("www.kohls.com,www.macys.com"));
final Pair jeans = new Pair<Text, Text>(new Text("jeans"), new Text("www.macys.com"));

assertThat(result)
 .isNotNull()
 .hasSize(6)
 .contains(clothes, jeans);
 }