Mockito:模拟包私人课程

时间:2014-12-23 08:26:39

标签: unit-testing mockito amazon-dynamodb

我有以下简单的DynamoDBDao,其中包含一个查询索引并返回结果映射的方法。

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;
    }
}

我正在尝试编写一个单元测试,用于验证当DynamoDB索引返回ItemCollection时,Dao会返回相应的结果映射。

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));
    }
}

我的问题是items.iterator()实际上没有返回Iterator它返回一个IteratorSupport,它是DynamoDB文档API中的包私有类。这意味着我实际上无法像上面那样嘲笑它,所以我无法完成剩下的测试。

在这种情况下我该怎么办?如果在DynamoDB文档API中使用这个糟糕的包私有类,我如何正确地单元测试我的DAO?

6 个答案:

答案 0 :(得分:6)

首先,单元测试永远不应该尝试验证对象内部的私有状态。它可以改变。 如果该类没有通过非私有getter方法公开其状态,那么它的测试业务就不是它的实现方式。

其次,你为什么关心迭代器的实现? 该类通过返回迭代器(接口)来履行其合同 迭代时将返回它应该的对象。

第三,你为什么要嘲笑你不需要的东西? 构建模拟对象的输入和输出,不要嘲笑它们;这是不必要的。 您将表传递给构造函数?精细。
然后扩展Table类,为您需要的任何保护方法。 将受保护的getter和/或setter添加到Table子类中。 如有必要,让他们返回硬编码值。它们并不重要。

请记住,只测试测试类中的一个类。 您正在测试dao而不是表,也不测量索引。

答案 1 :(得分:1)

Dynamodb api有很多这样的类,不容易被嘲笑。这导致花费大量时间编写复杂的测试和更改功能是很大的痛苦。

我认为,对于这种情况,更好的方法是不要尝试采用传统方式并使用AWS团队使用DynamodbLocal库 - http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html

这基本上是DyanamoDB的内存实现。我们编写了测试,以便在单元测试初始化​​期间,将生成DyanmodbLocal实例并创建表。这使得测试变得轻而易举。我们尚未在库中发现任何错误,并且由AWS积极支持和开发。强烈推荐它。

答案 2 :(得分:0)

一种可能的解决方法是定义一个测试类,该测试类将IteratorSupport扩展到它所在的同一个包中,并定义所需的行为

然后,您可以通过测试用例中的模拟设置返回此类的实例。

当然,这不是一个好的解决方案,而只是一个解决方法,原因与@Jeff Bowman在评论中提到的相同。

答案 3 :(得分:0)

将ItemCollection检索提取到单独的方法可能会更好吗? 在您的情况下,它可能如下所示:

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);
  }
}

然后在单元测试中:

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;
  }
}

答案 4 :(得分:0)

当您使用when(items.iterator()).thenReturn(iterator);时,Mockito会将项目视为ItemCollection,从而导致编译错误。在您的测试用例中,您希望将ItemCollection视为Iterable。因此,简单的解决方案是将项目转换为Iterable,如下所示:

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

还要将迭代器设为

@Mock
Iterator<QueryOutcome> iterator;

这应该在没有警告的情况下修复代码:)

如果这样可以解决问题,请接受答案。

答案 5 :(得分:0)

您可以使用以下虚假对象测试您的读取方法:

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;
    }
}