Reputation: 1165
i am writing a integration test framework for my java application and i run into one issue i am not able to fix. Please let me explain:
The following code snippets are simplified classes for better visibility:
I got the following abstract class:
public abstract class AbstractTestModel<T extends AbstractTestModel> {
public T doSomething(){
return getThis();
}
public T getThis(){
return (T) this;
}
public <U extends AbstractTestModel<T>> U as(Class<U> type){
AbstractTestModel<T> obj = this;
if (obj.getClass().isAssignableFrom(type)){
return type.cast(obj);
}else{
throw new AssertionError("This (" + obj.getClass().getName() +")could not be resolved to the expected class " + type.getName());
}
}
}
And there are may concrete classes like this:
public class ConcreteTestModel1 extends AbstractTestModel<ConcreteTestModel1> {
public void doSomethingElse(){
}
}
I also wrote a Factory class. This factory downloads a JSON object from the server and instantiates one of the concret classes - depending on the JSON response. This concrete classes have many helper methods for testing the JSON response. The thing is, that the factory method always returns an "AbstractTestModel".
The integration test looks like this (simplified):
public class SomeTest {
TestModelFactory testModelFactory;
@Before
public void init(){
testModelFactory = new TestModelFactory();
}
@Test
public void someTest(){
AbstractTestModel anyModel = testModelFactory.createModel("someIdOfTheServer");
//ERROR: this does not work! Cannot resolve method doSomethingElse():
anyModel.doSomething().as(ConcreteTestModel1.class).doSomethingElse();
//as() method returns AbstractTestModel instead of ConcreteTestModel1
//this works:
AbstractTestModel as = anyModel.as(ConcreteTestModel1.class);
//this does not work:
ConcreteTestModel1 asConcreteTestModel1 = anyModel.as(ConcreteTestModel1.class);
}
}
The method as(Class type) should check if the given class is valid, cast "this" to the desired class and return it, but it always returns the AbstractTestModel.
If I make the "as" method static or if i get rid of the generic class like this...
public abstract class AbstractTestModel {
/*
Not possible to return the superclass anymore
public T doSomething(){
return getThis();
}
public T getThis(){
return (T) this;
}
*/
public <U extends AbstractTestModel> U as(Class<U> type){
AbstractTestModel obj = this;
if (obj.getClass().isAssignableFrom(type)){
return type.cast(obj);
}else{
throw new AssertionError("This (" + obj.getClass().getName() +")could not be resolved to the expected class " + type.getName());
}
}
}
... then it works fine, but of course i am not able to return the concrete class in all other methods any more.
Thank you for reading this long post. Do you know what am i doing wrong here? Is there a solution for that?
Thank you for any tip and have a nice day!
Manuel
Upvotes: 1
Views: 6585
Reputation: 1165
Thanks to all answers, i could finally find the problem. It was actually quite easy:
The factory needed to return "AbstractTestModel<❓>" instead of just "AbstractTestModel". I've added the <❓> to every method who's returning the AbstractTestModel. Now it works just fine :) Thanks everyone. Couldn't find the answer without you.
Upvotes: 1
Reputation: 1115
The problem is the compiler needs to infer the type at compile time. This code is mostly as provided, but I added some output in doSomethingElse
for the sake of demonstration, and added ConcreteTestModelX
extending ConcreteTestModel1
(Note I removed the type T
from the as
method to explore the way it interacted with generic typing in further exploratory testing).
public abstract class AbstractTestModel<T extends AbstractTestModel> {
public T doSomething() {
return getThis();
}
public T getThis() {
return (T) this;
}
public <U extends AbstractTestModel> U as(Class<U> type) {
if (getClass().isAssignableFrom(type)) {
return type.cast(this);
} else {
throw new AssertionError("This (" + getClass().getName()
+ ") could not be resolved to the expected class " + type.getName());
}
}
}
class ConcreteTestModel1 extends AbstractTestModel<ConcreteTestModel1> {
public void doSomethingElse() {
System.out.println("This is \"" + getClass().getSimpleName() + "\" doing something else");
}
}
class ConcreteTestModelX extends ConcreteTestModel1 {
}
And with this test
import org.junit.Test;
public class SomeTest {
@Test
public void someTest(){
AbstractTestModel<ConcreteTestModel1> anyModel = new ConcreteTestModel1();
ConcreteTestModel1 asConcreteTestModel1 = anyModel.as(ConcreteTestModel1.class);
asConcreteTestModel1.doSomethingElse();
AbstractTestModel anyModelX = new ConcreteTestModelX();
ConcreteTestModel1 asConcreteTestModelX = (ConcreteTestModel1)anyModelX;
asConcreteTestModelX.doSomethingElse();
}
}
Seems the problem you are having in the tests is that the variable you are using for the model is without generics, the compiler then strips the generics see this answer https://stackoverflow.com/a/18277337/7421645
Based on this I then created some new tests to explore:
import org.junit.Test;
public class SomeTest {
@Test
public void concreteTest(){
ConcreteTestModel1 asConcreteTestModel1 = getConcreteModel(new ConcreteTestModel1(), ConcreteTestModel1.class);
asConcreteTestModel1.doSomethingElse();
}
@Test
public void concreteExtendsTest(){
ConcreteTestModel1 asConcreteTestModelX = getConcreteModel(new ConcreteTestModelX(), ConcreteTestModelX.class);
asConcreteTestModelX.doSomethingElse();
}
private <T extends ConcreteTestModel1> T getConcreteModel(T anyModel, Class<T> classType) {
return anyModel.as(classType);
}
@Test
public void vanillaCastingTest(){
AbstractTestModel anyModelX = new ConcreteTestModelX();
ConcreteTestModel1 asConcreteTestModelX = (ConcreteTestModel1)anyModelX;
asConcreteTestModelX.doSomethingElse();
}
@Test
public void abstractGenericTest(){
AbstractTestModel<ConcreteTestModel1> anyModel = new ConcreteTestModel1();
ConcreteTestModel1 asConcreteTestModel1 = anyModel.as(ConcreteTestModel1.class);
asConcreteTestModel1.doSomethingElse();
}
@Test
public void failedGenericTest(){
AbstractTestModel anyModel = new ConcreteTestModel1();
ConcreteTestModel1 asConcreteTestModel1 = getAs(anyModel);
asConcreteTestModel1.doSomethingElse();
}
private ConcreteTestModel1 getAs(AbstractTestModel<ConcreteTestModel1> anyModel) {
return anyModel.as(ConcreteTestModel1.class);
}
}
Upvotes: 1