mercredi 15 août 2018

Unit testing with Dagger and AWS Lambda

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 the onCreate method or the main 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

  1. Have I set up Dagger correctly?
  2. If not, how should I have set it up?
  3. Is it correct to build the AppComponent in the constructor of my Lambda function?
  4. Is it possible to unit test like I want to?
  5. If so, how should I set up Dagger for this testing?

Aucun commentaire:

Enregistrer un commentaire