Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using DaggerMock with inject() method #86

Open
stuartleylandcole opened this issue Sep 7, 2018 · 2 comments
Open

Using DaggerMock with inject() method #86

stuartleylandcole opened this issue Sep 7, 2018 · 2 comments

Comments

@stuartleylandcole
Copy link

stuartleylandcole commented Sep 7, 2018

Hi! Thanks for a great library, this is going to make my testing much, much easier 😃

I am trying to test a class which has its dependencies configured by Dagger by calling an inject() method on the Dagger component and passing itself in. Does DaggerMock support mocking of dependencies when a class is initialised like this? If so, how should I do this?

In case it isn't clear what I'm asking, here are some relevant code excerpts from my project to illustrate my configuration. This is my first use of Dagger so it's entirely possible I may be misusing it, leading to my current problems.

ListMealsHandler.java - class under test

public class ListMealsHandler implements RequestHandler<ApiGatewayRequest, ApiGatewayResponse> {

    @Inject
    MealRepository repository;

    public ListMealsHandler() {
        final AppComponent component = DaggerAppComponent.builder().build();
        component.inject(this);
    }

    @Override
    public ApiGatewayResponse handleRequest(final ApiGatewayRequest request, final Context context) {
    //other, irrelevant code
    }
}

AppComponent.java - Dagger configuration

@Singleton
@Component(modules = { InfrastructureModule.class, DaoModule.class })
public interface AppComponent {

    void inject(ListMealsHandler handler);
}

ListMealsHandlerTest.java - unit test for handler

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

    @Named("mealsTableName")
    String mealsTableName = "meals";

    @Named("awsRegion")
    String awsRegion = "eu-west-1";

    @Rule
    public DaggerMockRule<AppComponent> rule = new DaggerMockRule<AppComponent>(AppComponent.class, new InfrastructureModule())
            .set(new ComponentSetter<AppComponent>() {

                @Override
                public void setComponent(final AppComponent component) {
                    component.inject(handler);
                }
            });

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

I want to use DaggerMock to provide the values for @Named("mealsTableName") and @Named("awsRegion") because these are defined in InfrastructureModule.java by referencing system variables which of course aren't set in unit tests:

InfrastructureModule.java

package com.mealplanner.config;

import javax.inject.Named;
import javax.inject.Singleton;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;

import dagger.Module;
import dagger.Provides;

@Module
public class InfrastructureModule {

    @Singleton
    @Provides
    @Named("mealsTableName")
    public String tableName() {
        return System.getenv("tableName");
    }

    @Singleton
    @Provides
    @Named("awsRegion")
    public String awsRegion() {
        return System.getenv("region");
    }
}

The above configuration gives me a NullPointerException when calling the handler on this line:

final ApiGatewayResponse response = handler.handleRequest(request, context);

How should I set up Dagger/DaggerMock to support what I want to do?

Thanks again for this library and in advance for any help you can offer 😀

Edit

I have just switched to using JUnit 4 instead of 5 to run the tests and I'm getting a different exception from Dagger code, a NullPointerException from the code below:

ListMealsHandler_MembersInjector.java

package com.mealplanner.function;

import com.mealplanner.dal.MealRepository;
import dagger.MembersInjector;
import javax.annotation.Generated;
import javax.inject.Provider;

@Generated(
  value = "dagger.internal.codegen.ComponentProcessor",
  comments = "https://google.github.io/dagger"
)
public final class ListMealsHandler_MembersInjector implements MembersInjector<ListMealsHandler> {
  private final Provider<MealRepository> repositoryProvider;

  public ListMealsHandler_MembersInjector(Provider<MealRepository> repositoryProvider) {
    this.repositoryProvider = repositoryProvider;
  }

  public static MembersInjector<ListMealsHandler> create(
      Provider<MealRepository> repositoryProvider) {
    return new ListMealsHandler_MembersInjector(repositoryProvider);
  }

  @Override
  public void injectMembers(ListMealsHandler instance) {
    injectRepository(instance, repositoryProvider.get());
  }

  public static void injectRepository(ListMealsHandler instance, MealRepository repository) {
    instance.repository = repository; //NPE here
  }
}

From this I can maybe learn two things:

  1. DaggerMock doesn't work with JUnit 5
  2. I'm not setting up Dagger/my tests/something correctly

Any help you can provide on the above would be greatly appreciated!

@fabioCollini
Copy link
Owner

Hi @stuartleylandcole ,
I have never used DaggerMock with JUnit 5, I'll try to use it soon. Thanks for the report!
Based on your example I think that the problem is that you create the component and inject in the ListMealsHandler constructor. So even in the test you create the real component and use it to inject your object. Then you'll inject it again with the DaggerMock component but it's too late, you have already got the error.
I think you can use something similar to what we do in Android, you can store the component in an external singleton. In ListMealsHandler constructor you can retrieve the component from this singleton and inject the object. From the test you manually replace the real component with the DaggerMock component (you need to pay attention and avoid the real component creation if it's already been set by the test).
Hope this helps you, let me know if it works in this way

@reachout2ashu
Copy link

hi @stuartleylandcole : Use this library to fix your issue: https://stefanbirkner.github.io/system-rules/
I had exact same issue and found a solution with above mentioned library:

@rule
public final EnvironmentVariables environmentVariables = new EnvironmentVariables();
environmentVariables.set("KEY", "VALUE");

gradle dependency:
testImplementation 'com.github.stefanbirkner:system-rules:1.19.0'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants