Testing Pakour jobs

August 17, 2014 ยท View on GitHub

Like most Hadoop libraries, Pakour supports testing jobs by running jobs in local mode. Unlike most libraries, local access to dseqs and dsinks makes input-construction and result-checking nearly as straightforward as testing non-MapReduce code.

Configuration

Local-mode tests require a local-mode Hadoop configuration, which should override the default configuration in e.g. a cluster-connected REPL. In the parkour.test-helpers namespace Parkour provides a with-config macro and config-fixture clojure.test-style fixture-function for scoping tests to such a configuration. They provide a dynamic scope in which a test-optimized local-only Hadoop configuration becomes the default and a dynamic scope for cleaning transient resources. All Parkour application tests should run under one of these facilities.

(require '[parkour.test-helpers :as th])

(use-fixtures :once th/config-fixture)

;; or

(deftest test-something
  (th/with-config
    ...))

Input/Output

Parkour allows seamless testing of job results exactly as a regular program would consume job results. Parkour does provide a small number of test-support functions primarily designed to simplify construction of test inputs.

with-dseq

The parkour.io.dsink namespace with-dseq function writes a Clojure collection to a dsink, using the same backing Hadoop OutputFormat the dsink configures an actual Hadoop job to use. The function then yields the associated dseq, which may be used as input for a job or job graph.

(deftest test-word-count-local
  (th/with-config
    (let [inpath (doto (fs/path "tmp/word-count-input") fs/path-delete)
          outpath (doto (fs/path "tmp/word-count-output") fs/path-delete)
          dseq (dsink/with-dseq (text/dsink inpath)
                 (->> ["apple banana banana" "carrot apple" "apple"]
                      (mr/sink-as :keys)))
          dsink (seqf/dsink [Text LongWritable] outpath)
          [result] (word-count (conf/ig) dseq dsink)]
      (is (= {"apple" 3, "banana" 2, "carrot" 1}
             (into {} result))))))

mem/dseq

The parkour.io.mem namespace dseq function generates dseqs which use a memory-backed Hadoop InputFormat to produce tuples from arbitrary in-memory Clojure collections. This circumvents normal Hadoop serialization, but can simplify testing of jobs normally consuming from more esoteric and/or non-HDFS input formats.

(deftest test-word-count-mem
  (th/with-config
    (let [outpath (doto (fs/path "tmp/word-count-output") fs/path-delete)
          dseq (->> [["apple banana banana"]
                     ["carrot apple"]
                     ["apple"]]
                    (mem/dseq :keys))
          dsink (seqf/dsink [Text LongWritable] outpath)
          [result] (word-count (conf/ig) dseq dsink)]
      (is (= {"apple" 3, "banana" 2, "carrot" 1}
             (into {} result))))))

Partitioners

Hadoop local mode only allows a single reducer, which prevents any job custom partitioners from even running. Although not ideal, any custom partitioners should be unit-tested directly, independently of job execution. Fortunately, Parkour partitioners are just functions, and relatively easy to so-test.