ClassCastException при имитации универсального типа с использованием Mockito

У меня есть абстрактный тестовый класс с универсальным параметром.

public abstract class AbstractCarTest<G> {
    ...
    @Mock
    protected G carMock;
    ...
}

Я реализовал конкретный тестовый класс этого.

public class TruckTest extends AbstractCarTest<Truck> {
    ...
    when(truckFactory.getTruck(anyString()).return(carMock);
    ...
}

Подпись метода выглядит следующим образом

public Truck getTruck(String name);

При беге TruckTest Я получаю ClassCastException поговорка

java.lang.ClassCastException: org.mockito.internal.creation.jmock.ClassImposterizer$ClassWithSuperclassToWorkAroundCglibBug$$EnhancerByMockitoWithCGLIB$$... cannot be cast to com.example.Truck

Это почему? Как я могу обойти это?

3 ответа

Mockito генерирует ваш фиктивный класс во время выполнения. Моты создаются путем создания подкласса данного класса и переопределения всех его методов. (Таким образом, ограничение не издеваться final классы или методы.) Во время выполнения все универсальные типы стираются и заменяются их верхней границей типа в вашем AbstractCarTest это Object так как вы не указываете явную верхнюю границу. Поэтому Mockito видит ваш необработанный класс как:

public abstract class AbstractCarTest {

  @Mock
  protected Object carMock;
}

во время выполнения и создаст макет, который расширяет Object вместо желаемого Truck учебный класс. (Вы не можете увидеть это, потому что в cglib есть ошибка, которая не может быть расширена Object непосредственно. Вместо этого Mockito расширяет внутренний класс, называемый ClassWithSuperclassToWorkAroundCglibBug.) Обычно компилятор выдаст ошибку типа во время компиляции, когда универсальный тип все еще доступен, но во время выполнения вместо этого возникает загрязнение кучи.

Обходной путь будет следующим:

public abstract class AbstractCarTest<T> {

  protected abstract T getCarMock();

  // define some base test cases here that use the generic type.
}

public class TruckTest extends AbstractCarTest<Truck> {

  @Mock
  private Truck carMock;

  @Override
  protected Truck getCarMock() { return carMock; }

  // ...
  when(truckFactory.getTruck(anyString()).return(getCarMock());
}

Определяя ваш смоделированный тип без использования обобщений, вы можете получить доступ к макету из абстрактного базового класса с помощью метода получения, где макетированный тип правильно определен как Truck,

Кажется, что Мокито не будет издеваться над классом, когда он не знает, что это конкретный тип.

Вы можете обойти проблему, выполнив следующие действия:

public abstract class AbstractCarTest<G> {
    ...
    protected G carMock;
    ...

    @Before
    public void setUp() throws Exception {
        carMock= Mockito.mock(getClazz());
    }

    protected abstract Class<G> getClazz();
}


public class TruckTest extends AbstractCarTest<Truck> {
    ...
    when(truckFactory.getTruck(anyString()).return(carMock);
    ...

    @Override
    protected Class<Truck> getClazz() {
        return Truck.class;
    }
}

Просто хотел немного улучшить ответ от @geoand.

public abstract class AbstractCarTest<G> {
    ...
    protected G carMock;
    ...

    @Before
    public void setUp() throws Exception {
        carMock= Mockito.mock(getClazz());
    }

    private Class<G> getClazz() {
        return (Class<G>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }
}


public class TruckTest extends AbstractCarTest<Truck> {
    ...
    when(truckFactory.getTruck(anyString()).return(carMock);
    ...
}

Это в конечном итоге устраняет необходимость в абстрактном методе. Индекс предоставлен getActualTypeArguments() будет зависеть от того, где аргумент типа появится в вашей декларации. В большинстве случаев это будет 0 Однако, если у вас есть что-то вроде этого:

public abstract class AbstractCarTest<A, B, C, G>

И вы хотели использовать Class из G вам нужно будет поставить и индекс 3,

Другие вопросы по тегам