DaggerMock
DaggerMock copied to clipboard
Using DaggerMock with inject() method
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:
- DaggerMock doesn't work with JUnit 5
- I'm not setting up Dagger/my tests/something correctly
Any help you can provide on the above would be greatly appreciated!
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
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'