I am trying to set up Dagger in my Serverless project which has a number of Java AWS Lambda implementations. The Lambda code is relatively straightforward and mostly deals with reading the request and writing the response, with service classes doing most of the heavy lifting.
I would like to unit test my Lambda code but I'm having some trouble setting up Dagger for this. The specific problem I'm having is that I rely on an environment variable to specify the AWS region to construct a DynamoDB client. When running unit tests this environment variable is null, causing the builder to throw an exception. This is the full exception (which hopefully makes some sense with the code below):
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
I have looked at DaggerMock to provide mocks for my objects but I've failed to get it working.
Here are the pieces of code which set up Dagger and other relevant classes:
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
See notes below code snippet for some explanations.
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
}
}
- I have included an injectable field for
repository
purely for testing, ideally I would have the dependency injected into the constructor - I create the
AppComponent
in the constructor as this seems to be the "entry" point for Lambdas. I understand that you would normally set this up in theonCreate
method or themain
method but I don't think I have access to these for Lambdas.
And this is how I'd like to test the Lambda function, i.e. mock the dependencies and perform assertions based on that.
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);
}
}
Questions
- Have I set up Dagger correctly?
- If not, how should I have set it up?
- Is it correct to build the
AppComponent
in the constructor of my Lambda function? - Is it possible to unit test like I want to?
- If so, how should I set up Dagger for this testing?
Aucun commentaire:
Enregistrer un commentaire