Hoewel de acceptatie van Apache HBase voor het bouwen van applicaties voor eindgebruikers enorm is gestegen, zijn veel van die applicaties (en veel apps in het algemeen) niet goed getest. In dit bericht leer je enkele manieren waarop dit testen gemakkelijk kan worden gedaan.
We beginnen met unit-testing via JUnit, gaan dan verder met het gebruik van Mockito en Apache MRUnit en vervolgens met het gebruik van een HBase-minicluster voor integratietests. (De HBase-codebase zelf wordt getest via een minicluster, dus waarom zou u daar ook niet gebruik van maken voor upstream-toepassingen?)
Laten we als basis voor discussie aannemen dat u een HBase-gegevenstoegangsobject (DAO) hebt dat het volgende in HBase invoegt. De logica kan natuurlijk ingewikkelder zijn, maar voor het voorbeeld, dit doet het werk.
public class MyHBaseDAO { public static void insertRecord(HTableInterface table, HBaseTestObj obj) throws Exception { Put put = createPut(obj); table.put(put); } private static Put createPut(HBaseTestObj obj) { Put put = new Put(Bytes.toBytes(obj.getRowKey())); put.add(Bytes.toBytes("CF"), Bytes.toBytes("CQ-1"), Bytes.toBytes(obj.getData1())); put.add(Bytes.toBytes("CF"), Bytes.toBytes("CQ-2"), Bytes.toBytes(obj.getData2())); return put; } }
HBaseTestObj is een basisgegevensobject met getters en setters voor rowkey, data1 en data2.
De insertRecord voegt een insert toe aan de HBase-tabel tegen de kolomfamilie van CF, met CQ-1 en CQ-2 als kwalificaties. De methode createPut vult eenvoudig een Put in en retourneert deze naar de aanroepende methode.
JUnit gebruiken
JUnit, dat op dit moment goed bekend is bij de meeste Java-ontwikkelaars, kan eenvoudig worden toegepast op veel HBase-applicaties. Voeg eerst de afhankelijkheid toe aan je pom:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency>
Nu, binnen de testklasse:
public class TestMyHbaseDAOData { @Test public void testCreatePut() throws Exception { HBaseTestObj obj = new HBaseTestObj(); obj.setRowKey("ROWKEY-1"); obj.setData1("DATA-1"); obj.setData2("DATA-2"); Put put = MyHBaseDAO.createPut(obj); assertEquals(obj.getRowKey(), Bytes.toString(put.getRow())); assertEquals(obj.getData1(), Bytes.toString(put.get(Bytes.toBytes("CF"), Bytes.toBytes("CQ-1")).get(0).getValue())); assertEquals(obj.getData2(), Bytes.toString(put.get(Bytes.toBytes("CF"), Bytes.toBytes("CQ-2")).get(0).getValue())); } }
Wat u hier deed, was ervoor zorgen dat uw createPut-methode een Put-object met verwachte waarden maakt, vult en retourneert.
Mockito gebruiken
Dus hoe ga je om met het testen van de bovenstaande insertRecord-methode? Een zeer effectieve aanpak is om dit te doen met Mockito.
Voeg eerst Mockito als afhankelijkheid toe aan je pom:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> <scope>test</scope> </dependency>
Dan, in de testklas:
@RunWith(MockitoJUnitRunner.class) public class TestMyHBaseDAO{ @Mock private HTableInterface table; @Mock private HTablePool hTablePool; @Captor private ArgumentCaptor putCaptor; @Test public void testInsertRecord() throws Exception { //return mock table when getTable is called when(hTablePool.getTable("tablename")).thenReturn(table); //create test object and make a call to the DAO that needs testing HBaseTestObj obj = new HBaseTestObj(); obj.setRowKey("ROWKEY-1"); obj.setData1("DATA-1"); obj.setData2("DATA-2"); MyHBaseDAO.insertRecord(table, obj); verify(table).put(putCaptor.capture()); Put put = putCaptor.getValue(); assertEquals(Bytes.toString(put.getRow()), obj.getRowKey()); assert(put.has(Bytes.toBytes("CF"), Bytes.toBytes("CQ-1"))); assert(put.has(Bytes.toBytes("CF"), Bytes.toBytes("CQ-2"))); assertEquals(Bytes.toString(put.get(Bytes.toBytes("CF"), Bytes.toBytes("CQ-1")).get(0).getValue()), "DATA-1"); assertEquals(Bytes.toString(put.get(Bytes.toBytes("CF"), Bytes.toBytes("CQ-2")).get(0).getValue()), "DATA-2"); } }
Hier hebt u HBaseTestObj ingevuld met "ROWKEY-1", "DATA-1", "DATA-2" als waarden. Vervolgens heb je de bespotte tabel en de DAO gebruikt om het record in te voegen. Je hebt de Put vastgelegd die de DAO zou hebben ingevoegd en geverifieerd dat de rowkey, data1 en data2 zijn wat je ervan verwacht.
De sleutel hier is om het maken van htable-pool en htable-instantie buiten de DAO te beheren. Hierdoor kunt u ze netjes bespotten en Puts testen zoals hierboven weergegeven. Op dezelfde manier kunt u nu uitbreiden naar alle andere bewerkingen zoals Ophalen, Scannen, Verwijderen, enzovoort.
MRUnit gebruiken
Nu de reguliere tests voor gegevenstoegangseenheden zijn gedekt, gaan we ons richten op MapReduce-taken die in strijd zijn met HBase-tabellen.
Het testen van MR-taken die in strijd zijn met HBase is net zo eenvoudig als het testen van reguliere MapReduce-taken. MRUnit maakt het heel eenvoudig om MapReduce-taken te testen, inclusief de HBase-taken.
Stel je voor dat je een MR-taak hebt die schrijft naar een HBase-tabel, "MyTest", die één kolomfamilie "CF" heeft. Het verloop van zo'n baan zou er als volgt uit kunnen zien:
public class MyReducer extends TableReducer<Text, Text, ImmutableBytesWritable> { public static final byte[] CF = "CF".getBytes(); public static final byte[] QUALIFIER = "CQ-1".getBytes(); public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { //bunch of processing to extract data to be inserted, in our case, lets say we are simply //appending all the records we receive from the mapper for this particular //key and insert one record into HBase StringBuffer data = new StringBuffer(); Put put = new Put(Bytes.toBytes(key.toString())); for (Text val : values) { data = data.append(val); } put.add(CF, QUALIFIER, Bytes.toBytes(data.toString())); //write to HBase context.write(new ImmutableBytesWritable(Bytes.toBytes(key.toString())), put); } }
Hoe ga je nu om met het testen van de bovenstaande reducer in MRUnit? Voeg eerst MRUnit toe als afhankelijkheid aan je pom.
<dependency> <groupId>org.apache.mrunit</groupId> <artifactId>mrunit</artifactId> <version>1.0.0 </version> <scope>test</scope> </dependency>
Gebruik vervolgens binnen de testklasse de ReduceDriver die MRunit biedt, zoals hieronder:
public class MyReducerTest { ReduceDriver<Text, Text, ImmutableBytesWritable, Writable> reduceDriver; byte[] CF = "CF".getBytes(); byte[] QUALIFIER = "CQ-1".getBytes(); @Before public void setUp() { MyReducer reducer = new MyReducer(); reduceDriver = ReduceDriver.newReduceDriver(reducer); } @Test public void testHBaseInsert() throws IOException { String strKey = "RowKey-1", strValue = "DATA", strValue1 = "DATA1", strValue2 = "DATA2"; List<Text> list = new ArrayList<Text>(); list.add(new Text(strValue)); list.add(new Text(strValue1)); list.add(new Text(strValue2)); //since in our case all that the reducer is doing is appending the records that the mapper //sends it, we should get the following back String expectedOutput = strValue + strValue1 + strValue2; //Setup Input, mimic what mapper would have passed //to the reducer and run test reduceDriver.withInput(new Text(strKey), list); //run the reducer and get its output List<Pair<ImmutableBytesWritable, Writable>> result = reduceDriver.run(); //extract key from result and verify assertEquals(Bytes.toString(result.get(0).getFirst().get()), strKey); //extract value for CF/QUALIFIER and verify Put a = (Put)result.get(0).getSecond(); String c = Bytes.toString(a.get(CF, QUALIFIER).get(0).getValue()); assertEquals(expectedOutput,c ); } }
Kortom, na een heleboel verwerking in MyReducer, heb je dat geverifieerd:
- De output is wat je verwacht.
- De Put die in HBase is ingevoegd, heeft "RowKey-1" als rijtoets.
- "DATADATA1DATA2" is de waarde voor de CF-kolomfamilie en de CQ-kolomkwalificatie.
U kunt Mappers die gegevens van HBase ophalen ook testen op een vergelijkbare manier met MapperDriver, of MR-taken testen die uit HBase lezen, gegevens verwerken en naar HDFS schrijven.
Een HBase-minicluster gebruiken
Nu gaan we kijken hoe we integratietesten kunnen aanpakken. HBase wordt geleverd met HBaseTestingUtility, wat het schrijven van integratietesten met een HBase-minicluster eenvoudig maakt. Om de juiste bibliotheken binnen te halen, zijn de volgende afhankelijkheden vereist in uw pom:
<dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-common</artifactId> <version>2.0.0-cdh4.2.0</version> <type>test-jar</type> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase</artifactId> <version>0.94.2-cdh4.2.0</version> <type>test-jar</type> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-hdfs</artifactId> <version>2.0.0-cdh4.2.0</version> <type>test-jar</type> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-hdfs</artifactId> <version>2.0.0-cdh4.2.0</version> <scope>test</scope> </dependency>
Laten we nu eens kijken hoe u een integratietest kunt uitvoeren voor de MyDAO-insert die in de inleiding is beschreven:
public class MyHBaseIntegrationTest { private static HBaseTestingUtility utility; byte[] CF = "CF".getBytes(); byte[] QUALIFIER = "CQ-1".getBytes(); @Before public void setup() throws Exception { utility = new HBaseTestingUtility(); utility.startMiniCluster(); } @Test public void testInsert() throws Exception { HTableInterface table = utility.createTable(Bytes.toBytes("MyTest"), Bytes.toBytes("CF")); HBaseTestObj obj = new HBaseTestObj(); obj.setRowKey("ROWKEY-1"); obj.setData1("DATA-1"); obj.setData2("DATA-2"); MyHBaseDAO.insertRecord(table, obj); Get get1 = new Get(Bytes.toBytes(obj.getRowKey())); get1.addColumn(CF, CQ1); Result result1 = table.get(get1); assertEquals(Bytes.toString(result1.getRow()), obj.getRowKey()); assertEquals(Bytes.toString(result1.value()), obj.getData1()); Get get2 = new Get(Bytes.toBytes(obj.getRowKey())); get2.addColumn(CF, CQ2); Result result2 = table.get(get2); assertEquals(Bytes.toString(result2.getRow()), obj.getRowKey()); assertEquals(Bytes.toString(result2.value()), obj.getData2()); }}
Hier hebt u een HBase-minicluster gemaakt en gestart. Vervolgens heb je een tabel gemaakt met de naam "MyTest" met één kolomfamilie, "CF". Je hebt een record ingevoegd met de DAO die je moest testen, een Get uit dezelfde tabel gedaan en gecontroleerd of de DAO de records correct had ingevoegd.
Hetzelfde kan worden gedaan voor veel gecompliceerdere gebruiksscenario's, samen met de MR-taken zoals hierboven weergegeven. U kunt ook toegang krijgen tot de HDFS- en ZooKeeper-miniclusters die zijn gemaakt tijdens het maken van de HBase-versie, een MR-taak uitvoeren, die naar HBase uitvoeren en de ingevoegde records verifiëren.
Even een waarschuwing:het opstarten van een minicluster duurt 20 tot 30 seconden en kan niet op Windows zonder Cygwin. Omdat ze echter alleen periodiek moeten worden uitgevoerd, moet de langere looptijd acceptabel zijn.
U kunt voorbeeldcode voor de bovenstaande voorbeelden vinden op https://github.com/sitaula/HBaseTest. Veel plezier met testen!