Skip to content

Commit

Permalink
Fix checking of declared exceptions for methods in parent classes (#2547
Browse files Browse the repository at this point in the history
)

In case when parent contains throws keyword on its method and child overrides this method removing throws, it should be possible to mock throwing exception from child.

Fixes #2201

Co-authored-by: Andrey Kozel <andrey.kozel@kaseya.com>
  • Loading branch information
andrey-kozel and Andrey Kozel committed Jan 26, 2022
1 parent f8ffebe commit f6ce1bb
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 19 deletions.
Expand Up @@ -6,6 +6,9 @@

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.mockito.internal.invocation.AbstractAwareMethod;
import org.mockito.internal.util.MockUtil;
Expand All @@ -24,15 +27,52 @@ public InvocationInfo(InvocationOnMock theInvocation) {
this.invocation = theInvocation;
}

public boolean isValidException(Throwable throwable) {
Class<?>[] exceptions = method.getExceptionTypes();
Class<?> throwableClass = throwable.getClass();
for (Class<?> exception : exceptions) {
public boolean isValidException(final Throwable throwable) {
if (isValidException(method, throwable)) {
return true;
}

return isValidExceptionForParents(method.getDeclaringClass(), throwable);
}

private boolean isValidExceptionForParents(final Class<?> parent, final Throwable throwable) {
final List<Class<?>> ancestors = new ArrayList<>(Arrays.asList(parent.getInterfaces()));

if (parent.getSuperclass() != null) {
ancestors.add(parent.getSuperclass());
}

final boolean validException =
ancestors.stream()
.anyMatch(ancestor -> isValidExceptionForClass(ancestor, throwable));

if (validException) {
return true;
}

return ancestors.stream()
.anyMatch(ancestor -> isValidExceptionForParents(ancestor, throwable));
}

private boolean isValidExceptionForClass(final Class<?> parent, final Throwable throwable) {
try {
final Method parentMethod =
parent.getMethod(this.method.getName(), this.method.getParameterTypes());
return isValidException(parentMethod, throwable);
} catch (NoSuchMethodException e) {
// ignore interfaces that doesn't have such a method
return false;
}
}

private boolean isValidException(final Method method, final Throwable throwable) {
final Class<?>[] exceptions = method.getExceptionTypes();
final Class<?> throwableClass = throwable.getClass();
for (final Class<?> exception : exceptions) {
if (exception.isAssignableFrom(throwableClass)) {
return true;
}
}

return false;
}

Expand Down
Expand Up @@ -30,6 +30,7 @@ public class InvocationBuilder {
private int sequenceNumber = 0;
private Object[] args = new Object[] {};
private Object mock = Mockito.mock(IMethods.class);
private Class<?> mockClass = IMethods.class;
private Method method;
private boolean verified;
private List<Class<?>> argTypes;
Expand Down Expand Up @@ -57,7 +58,7 @@ public Invocation toInvocation() {

try {
method =
IMethods.class.getMethod(
mockClass.getMethod(
methodName, argTypes.toArray(new Class[argTypes.size()]));
} catch (Exception e) {
throw new RuntimeException(
Expand Down Expand Up @@ -115,6 +116,12 @@ public InvocationBuilder mock(Object mock) {
return this;
}

public InvocationBuilder mockClass(Class<?> mockClass) {
this.mockClass = mockClass;
this.mock = mock(mockClass);
return this;
}

public InvocationBuilder method(Method method) {
this.method = method;
return this;
Expand Down
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2022 Mockito contributors
* This program is made available under the terms of the MIT License.
*/
package org.mockito.internal.stubbing.answers;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.internal.invocation.InvocationBuilder;
import org.mockito.invocation.Invocation;

import java.nio.charset.CharacterCodingException;
import java.util.Arrays;
import java.util.Collection;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(Parameterized.class)
public class InvocationInfoExceptionTest {

private final String methodName;

public InvocationInfoExceptionTest(final String methodName) {
this.methodName = methodName;
}

@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(
new Object[][] {
{"throwException"},
{"parentThrowsException"},
{"grandParentThrowsException"},
{"interfaceThrowsException"},
{"grandInterfaceThrowsException"},
{"interfaceOfParentThrowsException"}
});
}

@Test
public void should_know_valid_throwables() throws Exception {
// when
final Invocation invocation =
new InvocationBuilder()
.method(methodName)
.mockClass(CurrentClass.class)
.toInvocation();
final InvocationInfo info = new InvocationInfo(invocation);

// then
assertThat(info.isValidException(new Exception())).isFalse();
assertThat(info.isValidException(new CharacterCodingException())).isTrue();
}

private abstract static class GrandParent {
public abstract void grandParentThrowsException() throws CharacterCodingException;
}

private interface InterfaceOfParent {
abstract void interfaceOfParentThrowsException() throws CharacterCodingException;
}

private abstract static class Parent extends GrandParent implements InterfaceOfParent {
public abstract void parentThrowsException() throws CharacterCodingException;
}

private interface GrandInterface {
void grandInterfaceThrowsException() throws CharacterCodingException;
}

private interface Interface extends GrandInterface {
void interfaceThrowsException() throws CharacterCodingException;
}

private static class CurrentClass extends Parent implements Interface {

public void throwException() throws CharacterCodingException {}

@Override
public void grandParentThrowsException() {}

@Override
public void parentThrowsException() {}

@Override
public void grandInterfaceThrowsException() {}

@Override
public void interfaceThrowsException() {}

@Override
public void interfaceOfParentThrowsException() {}
}
}
Expand Up @@ -9,26 +9,13 @@
import static org.mockitoutil.TestBase.getLastInvocation;

import java.lang.reflect.Method;
import java.nio.charset.CharacterCodingException;

import org.junit.Test;
import org.mockito.internal.invocation.InvocationBuilder;
import org.mockito.invocation.Invocation;
import org.mockitousage.IMethods;

public class InvocationInfoTest {

@Test
public void should_know_valid_throwables() throws Exception {
// when
Invocation invocation = new InvocationBuilder().method("canThrowException").toInvocation();
InvocationInfo info = new InvocationInfo(invocation);

// then
assertThat(info.isValidException(new Exception())).isFalse();
assertThat(info.isValidException(new CharacterCodingException())).isTrue();
}

@Test
public void should_know_valid_return_types() throws Exception {
assertThat(
Expand Down

0 comments on commit f6ce1bb

Please sign in to comment.