我正在尝试在我的Serverless项目中设置Dagger,该项目具有许多Java AWS Lambda实现。Lambda代码相对简单,主要涉及读取请求和编写响应,服务类负责大部分重活。
我想对Lambda代码进行单元测试,但是我在设置Dagger时遇到了一些问题。我遇到的具体问题是,我依赖于环境变量来指定AWS区域以构建DynamoDB客户端。在运行单元测试时,这个环境变量为空,导致构建器抛出异常。以下是完整的异常(希望下面的代码能够解释清楚):
我已经查看了DaggerMock以提供对象的模拟,但是我无法让它正常工作。
以下是设置Dagger和其他相关类的代码片段:
这就是我想测试Lambda函数的方式,即模拟依赖并基于此执行断言。
我想对Lambda代码进行单元测试,但是我在设置Dagger时遇到了一些问题。我遇到的具体问题是,我依赖于环境变量来指定AWS区域以构建DynamoDB客户端。在运行单元测试时,这个环境变量为空,导致构建器抛出异常。以下是完整的异常(希望下面的代码能够解释清楚):
org.mockito.exceptions.base.MockitoException:
Cannot instantiate @InjectMocks field named 'handler' of type 'class com.mealplanner.function.ListMealsHandler'.
You haven't provided the instance at field declaration so I tried to construct the instance.
However the constructor or the initialization block threw an exception : Could not find region information for 'null' in SDK metadata.
at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:165)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachCallbacks$0(TestMethodTestDescriptor.java:129)
at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:155)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachCallbacks(TestMethodTestDescriptor.java:128)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:107)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:58)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:113)
at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:121)
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
at java.util.Iterator.forEachRemaining(Iterator.java:116)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:121)
at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:121)
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
at java.util.Iterator.forEachRemaining(Iterator.java:116)
at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:121)
at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:55)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:43)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:154)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:90)
at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
Suppressed: java.lang.NullPointerException
at org.mockito.junit.jupiter.MockitoExtension.afterEach(MockitoExtension.java:211)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAfterEachCallbacks$11(TestMethodTestDescriptor.java:217)
at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeAllAfterMethodsOrCallbacks$13(TestMethodTestDescriptor.java:229)
at java.util.ArrayList.forEach(ArrayList.java:1249)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAllAfterMethodsOrCallbacks(TestMethodTestDescriptor.java:227)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeAfterEachCallbacks(TestMethodTestDescriptor.java:216)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:119)
... 46 more
Caused by: com.amazonaws.SdkClientException: Could not find region information for 'null' in SDK metadata.
at com.amazonaws.client.builder.AwsClientBuilder.getRegionObject(AwsClientBuilder.java:251)
at com.amazonaws.client.builder.AwsClientBuilder.withRegion(AwsClientBuilder.java:238)
at com.mealplanner.config.AppModule.providesAmazonDynamoDB(AppModule.java:17)
at com.mealplanner.config.AppModule_ProvidesAmazonDynamoDBFactory.proxyProvidesAmazonDynamoDB(AppModule_ProvidesAmazonDynamoDBFactory.java:34)
at com.mealplanner.config.AppModule_ProvidesAmazonDynamoDBFactory.provideInstance(AppModule_ProvidesAmazonDynamoDBFactory.java:25)
at com.mealplanner.config.AppModule_ProvidesAmazonDynamoDBFactory.get(AppModule_ProvidesAmazonDynamoDBFactory.java:21)
at com.mealplanner.config.AppModule_ProvidesAmazonDynamoDBFactory.get(AppModule_ProvidesAmazonDynamoDBFactory.java:1)
at com.mealplanner.dal.DynamoDbAdapter_Factory.provideInstance(DynamoDbAdapter_Factory.java:25)
at com.mealplanner.dal.DynamoDbAdapter_Factory.get(DynamoDbAdapter_Factory.java:21)
at com.mealplanner.dal.DynamoDbAdapter_Factory.get(DynamoDbAdapter_Factory.java:1)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
at com.mealplanner.dal.MealRepository_Factory.provideInstance(MealRepository_Factory.java:24)
at com.mealplanner.dal.MealRepository_Factory.get(MealRepository_Factory.java:20)
at com.mealplanner.dal.MealRepository_Factory.get(MealRepository_Factory.java:1)
at dagger.internal.DoubleCheck.get(DoubleCheck.java:47)
at com.mealplanner.config.DaggerAppComponent.getMealRepository(DaggerAppComponent.java:52)
at com.mealplanner.function.ListMealsHandler.<init>(ListMealsHandler.java:31)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.mockito.internal.util.reflection.FieldInitializer$NoArgConstructorInstantiator.instantiate(FieldInitializer.java:195)
at org.mockito.internal.util.reflection.FieldInitializer.acquireFieldInstance(FieldInitializer.java:137)
at org.mockito.internal.util.reflection.FieldInitializer.initialize(FieldInitializer.java:91)
at org.mockito.internal.configuration.injection.PropertyAndSetterInjection.initializeInjectMocksField(PropertyAndSetterInjection.java:94)
at org.mockito.internal.configuration.injection.PropertyAndSetterInjection.processInjection(PropertyAndSetterInjection.java:79)
at org.mockito.internal.configuration.injection.MockInjectionStrategy.process(MockInjectionStrategy.java:68)
at org.mockito.internal.configuration.injection.MockInjectionStrategy.relayProcessToNextStrategy(MockInjectionStrategy.java:89)
at org.mockito.internal.configuration.injection.MockInjectionStrategy.process(MockInjectionStrategy.java:71)
at org.mockito.internal.configuration.injection.MockInjectionStrategy.relayProcessToNextStrategy(MockInjectionStrategy.java:89)
at org.mockito.internal.configuration.injection.MockInjectionStrategy.process(MockInjectionStrategy.java:71)
at org.mockito.internal.configuration.injection.MockInjection$OngoingMockInjection.apply(MockInjection.java:92)
at org.mockito.internal.configuration.DefaultInjectionEngine.injectMocksOnFields(DefaultInjectionEngine.java:25)
at org.mockito.internal.configuration.InjectingAnnotationEngine.injectMocks(InjectingAnnotationEngine.java:87)
at org.mockito.internal.configuration.InjectingAnnotationEngine.processInjectMocks(InjectingAnnotationEngine.java:48)
at org.mockito.internal.configuration.InjectingAnnotationEngine.process(InjectingAnnotationEngine.java:42)
at org.mockito.MockitoAnnotations.initMocks(MockitoAnnotations.java:69)
at org.mockito.internal.framework.DefaultMockitoSession.<init>(DefaultMockitoSession.java:36)
at org.mockito.internal.session.DefaultMockitoSessionBuilder.startMocking(DefaultMockitoSessionBuilder.java:78)
... 52 more
我已经查看了DaggerMock以提供对象的模拟,但是我无法让它正常工作。
以下是设置Dagger和其他相关类的代码片段:
AppModule.java
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import dagger.Module;
import dagger.Provides;
@Module
public class AppModule {
private static final String AWS_REGION = System.getenv("region");
@Provides
public AmazonDynamoDB providesAmazonDynamoDB() {
return AmazonDynamoDBClientBuilder.standard()
.withRegion(AWS_REGION)
.build();
}
}
AppComponent.java
import javax.inject.Singleton;
import com.mealplanner.dal.DynamoDbAdapter;
import com.mealplanner.dal.MealRepository;
import dagger.Component;
@Singleton
@Component(modules = { AppModule.class })
public interface AppComponent {
DynamoDbAdapter getDynamoDbAdapter();
MealRepository getMealRepository();
}
DynamoDbAdapter.java
import javax.inject.Inject;
import javax.inject.Singleton;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
@Singleton
public class DynamoDbAdapter {
private final AmazonDynamoDB client;
@Inject
public DynamoDbAdapter(final AmazonDynamoDB client) {
this.client = client;
}
public DynamoDBMapper createDbMapper(final DynamoDBMapperConfig mapperConfig) {
return new DynamoDBMapper(client, mapperConfig);
}
}
MealRepository.java
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression;
import com.amazonaws.services.dynamodbv2.datamodeling.PaginatedQueryList;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.mealplanner.domain.Meal;
@Singleton
public class MealRepository {
private static final String MEALS_TABLE_NAME = System.getenv("tableName");
private final DynamoDBMapper mapper;
@Inject
public MealRepository(final DynamoDbAdapter dynamoDbAdapter) {
final DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder()
.withTableNameOverride(new DynamoDBMapperConfig.TableNameOverride(MEALS_TABLE_NAME))
.build();
this.mapper = dynamoDbAdapter.createDbMapper(mapperConfig);
}
}
ListMealsHandler.java
请看代码片段下面的注释以获取一些解释。import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.mealplanner.config.AppComponent;
import com.mealplanner.config.DaggerAppComponent;
import com.mealplanner.dal.MealRepository;
import com.mealplanner.domain.Meal;
import com.mealplanner.function.util.ApiGatewayRequest;
import com.serverless.ApiGatewayResponse;
public class ListMealsHandler implements RequestHandler<ApiGatewayRequest, ApiGatewayResponse> {
@Inject
MealRepository repository;
public ListMealsHandler() {
final AppComponent component = DaggerAppComponent.builder().build();
this.repository = component.getMealRepository();
}
@Override
public ApiGatewayResponse handleRequest(final ApiGatewayRequest request, final Context context) {
//read from request, call repository, and build response
}
}
- 我已经为
repository
包含了可注入的字段,纯粹是为了测试,理想情况下我希望将依赖项注入构造函数中。 - 我在构造函数中创建
AppComponent
,因为这似乎是Lambda的“入口”点。我知道通常你会在onCreate
方法或main
方法中设置它,但我认为对于Lambda来说我无法访问这些方法。
这就是我想测试Lambda函数的方式,即模拟依赖并基于此执行断言。
ListMealsHandlerTest.java
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.List;
import org.junit.Rule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.amazonaws.services.lambda.runtime.Context;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mealplanner.config.AppComponent;
import com.mealplanner.config.AppModule;
import com.mealplanner.dal.MealRepository;
import com.mealplanner.domain.Meal;
import com.mealplanner.function.ListMealsHandler;
import com.mealplanner.function.util.ApiGatewayRequest;
import com.mealplanner.function.util.Identity;
import com.mealplanner.function.util.RequestContext;
import com.serverless.ApiGatewayResponse;
@ExtendWith(MockitoExtension.class)
public class ListMealsHandlerTest {
private static final String USER_ID = "user1";
@Mock
private MealRepository mealRepository;
@Mock
private ApiGatewayRequest request;
@Mock
private RequestContext requestContext;
@Mock
private Identity identity;
@Mock
private Context context;
@InjectMocks
private ListMealsHandler handler;
@Test
public void all_users_meals_are_returned() throws Exception {
final List<Meal> meals = Arrays.asList();
when(mealRepository.getAllMealsForUser(USER_ID)).thenReturn(meals);
when(request.getRequestContext()).thenReturn(requestContext);
when(requestContext.getIdentity()).thenReturn(identity);
when(identity.getCognitoIdentityId()).thenReturn(USER_ID);
final ApiGatewayResponse response = handler.handleRequest(request, context);
final ObjectMapper objectMapper = new ObjectMapper();
final List<Meal> actualMeals = objectMapper.readValue(response.getBody(), new TypeReference<List<Meal>>() {
});
assertThat(actualMeals).containsExactlyInAnyOrderElementsOf(meals);
}
}
问题
- 我是否正确设置了Dagger?
- 如果没有,应该如何设置?
- 在Lambda函数的构造函数中构建
AppComponent 是否正确?
- 我是否可以按照所需方式进行单元测试?
- 如果可以,我应该如何为此测试设置Dagger?