2014-12-23 6 views
5

Ich habe das folgende einfache DynamoDBDao, die eine Methode enthält, die einen Index abfragt und eine Zuordnung der Ergebnisse zurückgibt.Mockito: Mocking-Paket private Klassen

import com.amazonaws.services.dynamodbv2.document.*; 

public class DynamoDBDao implements Dao{ 
    private Table table; 
    private Index regionIndex; 

    public DynamoDBDao(Table table) { 
     this.table = table; 
    } 

    @PostConstruct 
    void initialize(){ 
     this.regionIndex = table.getIndex(GSI_REGION_INDEX); 
    } 

    @Override 
    public Map<String, Long> read(String region) { 
     ItemCollection<QueryOutcome> items = regionIndex.query(ATTR_REGION, region); 
     Map<String, Long> results = new HashMap<>(); 
     for (Item item : items) { 
      String key = item.getString(PRIMARY_KEY); 
      long value = item.getLong(ATTR_VALUE); 
      results.put(key, value); 
     } 
     return results; 
    } 
} 

Ich versuche, eine Unit-Test zu schreiben, dass überprüft, wenn der DynamoDB Index eine ItemCollection kehrt dann das Dao die entsprechenden Ergebnisse Karte zurückgibt.

public class DynamoDBDaoTest { 

    private String key1 = "key1"; 
    private String key2 = "key2"; 
    private String key3 = "key3"; 
    private Long value1 = 1l; 
    private Long value2 = 2l; 
    private Long value3 = 3l; 

    @InjectMocks 
    private DynamoDBDao dynamoDBDao; 

    @Mock 
    private Table table; 

    @Mock 
    private Index regionIndex; 

    @Mock 
    ItemCollection<QueryOutcome> items; 

    @Mock 
    Iterator iterator; 

    @Mock 
    private Item item1; 

    @Mock 
    private Item item2; 

    @Mock 
    private Item item3; 

    @Before 
    public void setUp() { 
     MockitoAnnotations.initMocks(this); 
     when(table.getIndex(DaoDynamo.GSI_REGION_INDEX)).thenReturn(regionIndex); 
     dynamoDBDao.initialize(); 

     when(item1.getString(anyString())).thenReturn(key1); 
     when(item1.getLong(anyString())).thenReturn(value1); 
     when(item2.getString(anyString())).thenReturn(key2); 
     when(item2.getLong(anyString())).thenReturn(value2); 
     when(item3.getString(anyString())).thenReturn(key3); 
     when(item3.getLong(anyString())).thenReturn(value3); 
    } 

    @Test 
    public void shouldReturnResultsMapWhenQueryReturnsItemCollection(){ 

     when(regionIndex.query(anyString(), anyString())).thenReturn(items); 
     when(items.iterator()).thenReturn(iterator); 
     when(iterator.hasNext()) 
       .thenReturn(true) 
       .thenReturn(true) 
       .thenReturn(true) 
       .thenReturn(false); 
     when(iterator.next()) 
       .thenReturn(item1) 
       .thenReturn(item2) 
       .thenReturn(item3); 

     Map<String, Long> results = soaDynamoDbDao.readAll("region"); 

     assertThat(results.size(), is(3)); 
     assertThat(results.get(key1), is(value1)); 
     assertThat(results.get(key2), is(value2)); 
     assertThat(results.get(key3), is(value3)); 
    } 
} 

Mein Problem ist, dass items.iterator() tatsächlich Iterator nicht zurückgibt es eine IteratorSupport gibt, die ein Paket private Klasse in der DynamoDB Dokument API ist. Dies bedeutet, dass ich es nicht so verspotten kann wie ich es oben getan habe und deshalb kann ich den Rest meines Tests nicht abschließen.

Was kann ich in diesem Fall tun? Wie kann ich mein DAO richtig testen, wenn ich diese schreckliche private Paketklasse in der DynamoDB-Dokument-API verwende?

+1

Implementierungsdetails wie Sichtbarkeit sind einer der Gründe für die Richtlinie „[nicht Scheintypen, die Sie nicht besitzen] (http : //www.davesquared.net/2011/04/dont-mock-types-you-dont-own.html) ". Können Sie eine Abstraktion über eines dieser Objekte mit einem Vertrag/einer Implementierung schreiben, die Sie steuern, oder stattdessen mit einer Schnittstelle codieren? –

+0

Hallo Jeff, danke für deinen Kommentar. Ich sehe nicht, wie ich mit einem Vertrag/einer Implementierung, die ich kontrolliere, eine Abstraktion über diese Objekte schreiben kann. Ich habe mein aktuelles Toolset, das durch meine Kenntnisse und Erfahrungen begrenzt ist, erschöpft. Kannst du etwas sehen, was ich derzeit nicht kann? Wenn ja, wäre ich dankbar, wenn Sie mich in die richtige Richtung weisen könnten. –

Antwort

1

Dynamodb api hat viele solcher Klassen, die nicht leicht verspottet werden können. Dies hat zur Folge, dass viel Zeit für das Schreiben komplexer Tests und das Ändern von Features viel Schmerz bedeutet.

Ich denke, für diesen Fall ein besserer Ansatz wird versuchen, werden nicht den traditionellen Weg zu gehen und DynamodbLocal Bibliothek durch die AWS-Team nutzen - http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html

Dies ist im Grunde eine im Speicher Implementierung von DyanamoDB. Wir hatten unsere Tests so geschrieben, dass während der Initialisierung des Einheitentests die DyanmodbLocal-Instanz erzeugt wurde und Tabellen erstellt wurden. Dies macht den Test zum Kinderspiel. Wir haben noch keine Fehler in der Bibliothek gefunden und sie wird von AWS aktiv unterstützt und entwickelt. Sehr zu empfehlen.

5

Zuerst sollten Komponententests niemals versuchen, den privaten Zustand eines Objekts zu überprüfen. Es kann sich ändern. Wenn die Klasse ihren Status nicht über nicht-private Getter-Methoden verfügbar macht, dann ist es nicht Ihre Testaufgabe, wie sie implementiert wird.

Zweitens, warum kümmert es Sie, welche Implementierung der Iterator hat? Die Klasse hat ihren Vertrag erfüllt, indem sie einen Iterator (eine Schnittstelle) zurückgegeben hat, der beim Iterieren die Objekte zurückgibt, die er sein soll.

Drittens, warum verspotten Sie Objekte, die Sie nicht brauchen? Konstruiere die Ein- und Ausgänge für deine verspotteten Objekte, verspotte sie nicht; es ist unnötig. Sie übergeben eine Tabelle in Ihren Konstruktor? Fein.
Dann erweitern Sie die Tabelle Klasse, um alle geschützten Methoden für was auch immer Sie benötigen zu machen. Fügen Sie Ihrer Tabellen-Unterklasse geschützte Getter und/oder Setter hinzu. Lassen Sie sie bei Bedarf hart codierte Werte zurückgeben. Sie sind nicht wichtig.

Denken Sie daran, nur eine Klasse in Ihrer Testklasse zu testen. Sie testen den Dao nicht die Tabelle noch den Index.

+0

Leider muss der Serviceaufruf an DynamoDB gespottet werden und auf den Serviceaufruf kann nur durch polymorphen Zugriff auf paketgeschützte Klassen zugegriffen werden, deren Typen Java zur Laufzeit erzwingt. Anstatt zu fragen, warum, sollten Sie eine Lösung bieten. – Max

+0

Haben Sie versucht, den vom impliziten Aufruf zurückgegebenen Iterator in Ihrer for-Schleife zu verspotten? Überschreiben Sie ItemCollection, um ein Iterable zurückzugeben, das sich verhält, wie Sie es wünschen. –

+0

Das ist kein gültiges Java. Java überprüft den Typ zur Laufzeit. Eine beliebige Instanz von "Iterator" ist keine gültige Instanz von "IteratorSupprt", daher wird Java eine Laufzeitausnahme auslösen. Ich habe darüber ein Problem mit AWS und der Iterator ist nicht mehr paketgeschützt: https://github.com/aws/aws-sdk-java/issues/465 – Max

0

Eine mögliche Abhilfe ist es, eine Testklasse zu definieren, die IteratorSupport im selben Paket erweitert, die es in vorhanden ist, und definiert das gewünschte Verhalten

Sie dann eine Instanz dieser Klasse durch Ihren Mock-Setup in denen zurückkehren Testfall.

Natürlich ist dies keine gute Lösung, aber einfach eine Ausweichlösung aus den gleichen Gründen, die @Jeff Bowman in dem Kommentar erwähnt.

0

Ist es vielleicht besser, den ItemCollection-Abruf in die separate Methode zu extrahieren? In Ihrem Fall kann es wie folgt aussehen:

public class DynamoDBDao { 

    protected Iterable<Item> readItems(String region) { // can be overridden/mocked in unit tests 
    // ItemCollection implements Iterable, since ItemCollection-specific methods are not used in the DAO we can return it as Iterable instance 
    return regionIndex.query(ATTR_REGION, region); 
    } 
} 

dann in Unit-Tests:

private List<Item> mockItems = new ArrayList<>(); // so you can set these items in your test method 

private DynamoDBDao dao = new DynamoDBDao(table) { 
    @Override 
    protected Iterable<Item> readItems(String region) { 
    return mockItems; 
    } 
} 
0

Wenn Sie when(items.iterator()).thenReturn(iterator); Mockito die Einzelteile als ItemCollection sieht, die den Übersetzungsfehler verursacht. In Ihrem Testfall möchten Sie ItemCollection nur als Iterable sehen. So ist die einfache Lösung, die Elemente wie Iterable wie unten zu werfen:

when(((Iterable<QueryOutcome>)items).iterator()).thenReturn(iterator); 

Auch machen Sie Ihre Iterator als

@Mock 
Iterator<QueryOutcome> iterator; 

Dieser den Code ohne Warnung beheben sollte :)

Wenn diese Fehlerbehebungen das Problem, bitte akzeptieren Sie die Antwort.

0

Sie können mit gefälschten Objekte wie diese Ihre Lesemethode testen:

public class DynamoDBDaoTest { 

@Mock 
private Table table; 

@Mock 
private Index regionIndex; 


@InjectMocks 
private DynamoDBDao dynamoDBDao; 

public DynamoDBDaoTest() { 
} 

@Before 
public void setUp() { 
    MockitoAnnotations.initMocks(this); 
    when(table.getIndex(GSI_REGION_INDEX)).thenReturn(regionIndex); 
    dynamoDBDao.initialize(); 
} 

@Test 
public void shouldReturnResultsMapWhenQueryReturnsItemCollection() { 
    when(regionIndex.query(anyString(), anyString())).thenReturn(new FakeItemCollection()); 
    final Map<String, Long> results = dynamoDBDao.read("region"); 
    assertThat(results, allOf(hasEntry("key1", 1l), hasEntry("key2", 2l), hasEntry("key3", 3l))); 
} 

private static class FakeItemCollection extends ItemCollection<QueryOutcome> { 
    @Override 
    public Page<Item, QueryOutcome> firstPage() { 
     return new FakePage(); 
    } 
    @Override 
    public Integer getMaxResultSize() { 
     return null; 
    } 
} 

private static class FakePage extends Page<Item, QueryOutcome> { 
    private final static List<Item> items = new ArrayList<Item>(); 

    public FakePage() { 
     super(items, new QueryOutcome(new QueryResult())); 

     final Item item1= new Item(); 
     item1.with(PRIMARY_KEY, "key1"); 
     item1.withLong(ATTR_VALUE, 1l); 
     items.add(item1); 

     final Item item2 = new Item(); 
     item2.with(PRIMARY_KEY, "key2"); 
     item2.withLong(ATTR_VALUE, 2l); 
     items.add(item2); 

     final Item item3 = new Item(); 
     item3.with(PRIMARY_KEY, "key3"); 
     item3.withLong(ATTR_VALUE, 3l); 
     items.add(item3); 
    } 

    @Override 
    public boolean hasNextPage() { 
     return false; 
    } 

    @Override 
    public Page<Item, QueryOutcome> nextPage() { 
     return null; 
    } 
}