Unfortunately, due to type erasure, it isn't possible to do what I want. I will have to look at refactoring the existing code.
Injecting a genericized stub for testing purposes
-
11-07-2023 - |
Pergunta
I've currently got the following classes/interfaces laid out. The type T
represents the format of data returned from DataProvider
implementations. I'm using a factory so I don't need to attach type information to MyStreamingOutput
. I'm using HK2
to inject the DataProviderFactory
into MyStreamingOutput
.
public interface DataProvider<T> {
public T next() { ... }
...
}
public final class SQLDataProvider<T> {
public SQLDataProvider(final String query, final RowMapper<T> rowMapper) { ... }
}
public interface DataProviderFactory {
public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper);
...
}
public final class SQLDataProviderFactory {
public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper) {
return new SQLDataProvider<T>(query, rowMapper);
}
}
public final class MyStreamingOutput implements StreamingOutput {
public MyStreamingOutput(final DataProviderFactory dpFactory) { ... }
@Override public void write(final OutputStream outputStream) throws IOException { ... }
}
This all works fine. Now I'm trying to set up a unit test for MyStreamingOutput
, but I'm running into a couple of roadblocks. I wrote the following additional class for testing purposes:
public final class DataProviderFactoryStub implements DataProviderFactory {
private final DataProvider dataProvider;
public DataProviderFactoryStub() {
this.dataProvider = new DataProviderStub();
}
public DataProviderFactoryStub(final DataProvider dataProvider) {
this.dataProvider = dataProvider;
}
@Override
public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper) {
return this.dataProvider;
}
}
The binding occurs in
final class QueryTestResourceConfig extends ResourceConfig {
public QueryTestResourceConfig() {
...
this.register(new AbstractBinder() {
@Override
protected void configure() {
bind(DataProviderFactoryStub.class).to(DataProviderFactory.class);
}
});
}
}
I can successfully inject this class into MyStreamingOutput
, but it has a compiler warning because the typing information used by getDataProvider()
isn't shared by the instance passed into the factory. I can't add type information to the DataProviderFactoryStub
class because then it no longer implements the DataProviderFactory
interface. I don't want type information on the interface because it's wrong - outside of the Stub case, the factories shouldn't care about the type returned by DataProvider
instances. I'd very much like to avoid using setters for the query
and rowMapper
parameters because I consider it bad design in this case.
I can't shake the feeling that I'm either missing something subtle in my application of generics or something obvious in my application of dependency injection. What is the right way to address this use case? It seems like this is the kind of problem DI is meant to address, but I can't see how to fix it.
Solução 2
Outras dicas
When using DI, we usually end up with factory classes that are very basic (i.e., their creation methods are typically simple enough to fit on a single line). Your SQLDataProviderFactory
class is a perfect example of this.
The reason for this is because a factory object is just a placeholder for the creation of an object. We want to avoid littering our code with new
keywords, because doing so tightly couples code to a specific type. So we end up with factories whose methods are essentially just glorified new
keywords.
I bring this up to point out that it's the type of the product that is important here; the factory is just a conduit. When you replace a factory with a test double, what you're really doing is replacing a product with a test double. This means that whenever I define a test double factory, I always have to define a test double product as well.
For example, your stub factory is just trying to return a stub product. The problem is that the type of the stub product it's returning does not match the type expected by calling code. If you define your own stub product, the code falls into place:
public final class DataProviderStub<T> implements DataProvider<T> {
private final T dummy;
public DataProviderStub() { }
public T next() { return this.dummy; } // Just for example
}
public final class DataProviderFactoryStub implements DataProviderFactory {
public DataProviderFactoryStub() { }
@Override
public <T> DataProvider<T> getDataProvider(final String query, final RowMapper<T> rowMapper) {
return new DataProviderStub<T>();
}
}
The stub factory only exists so you can inject the stub DataProvider
into your SUT.