我有以下简单的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(( 实际上并没有返回迭代器,它返回了一个 IteratorSupport,它是 DynamoDB 文档 API 中的一个包私有类。这意味着我实际上不能像上面那样模拟它,所以我无法完成其余的测试。
在这种情况下我该怎么办?在 DynamoDB 文档 API 中给定这个糟糕的包私有类的情况下,如何正确对我的 DAO 进行单元测试?
首先,单元测试永远不应该尝试验证对象内部的私有状态。 它可以改变。如果该类没有通过非私有 getter 方法公开其状态,那么如何实现它与测试无关。
其次,你为什么关心迭代器有什么实现?该类通过返回迭代器(接口(来履行其协定当迭代时,它将返回它应该返回的对象。
第三,你为什么要嘲笑你不需要的对象?为模拟对象构建输入和输出,不要模拟它们;这是不必要的。您将表传递到构造函数中? 好。
然后扩展 Table 类,以便根据需要创建任何受保护的方法。将受保护的 getter 和/或 setter 添加到您的 Table 子类中。如有必要,让它们返回硬编码值。 他们无所谓。
请记住,在测试类中只测试一个类。您正在测试 dao,而不是表或索引。
Dynamodb api 有很多这样的类,它们不容易被嘲笑。这导致花费大量时间在编写复杂的测试上,并且更改功能是很大的痛苦。
我认为,对于这种情况,更好的方法是不要尝试采用传统方式并使用 AWS 团队的 DynamodbLocal 库 - http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html
这基本上是DyanamoDB的内存实现。我们已经编写了测试,以便在单元测试初始化期间,将生成 DyanmodbLocal 实例并创建表。这使得测试变得轻而易举。我们尚未在库中发现任何错误,并且它得到了 AWS 的积极支持和开发。强烈推荐它。
一种可能的解决方法是定义一个测试类,该测试类在它存在的同一包中扩展IteratorSupport
,并定义所需的行为
然后,您可以通过测试用例中的模拟设置返回此类的实例。
当然,这不是一个好的解决方案,而只是一种解决方法,原因与@Jeff Bowman在评论中提到的相同。
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;
}
}
当您使用when(items.iterator()).thenReturn(iterator);
Mockito时,会将项目视为ItemCollection,从而导致编译错误。在您的测试用例中,您希望将 ItemCollection 视为一个可迭代对象。因此,简单的解决方案是将项目转换为可迭代,如下所示:
when(((Iterable<QueryOutcome>)items).iterator()).thenReturn(iterator);
同时将迭代器设为
@Mock
Iterator<QueryOutcome> iterator;
这应该在没有警告的情况下修复代码:)
如果这解决了问题,请接受答案。
您可以使用如下的假对象来测试读取方法:
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;
}
}
ItemCollection<QueryOutcome> items = new ItemCollection<QueryOutcome>() {
@Override
public Integer getMaxResultSize() {
return 0;
}
@Override
public Page<Item, QueryOutcome> firstPage() {
return null;
}
};
Mockito.when(index.query(Mockito.any(QuerySpec.class))).thenReturn(items);
QueryResult queryResult = new QueryResult();
Mockito.when(dynamoDBClient.query(Mockito.any(QueryRequest.class))).thenReturn(queryResult);