Reputation: 12899
I need to test an abstract class in Java, and I have a method which depends on equals()
to provide a result.
public abstract class BaseClass<T extends BaseClass<T>> {
public boolean isCanonical() {
return toCanonical() == this;
}
public T toCanonical() {
T asCanonical = asCanonical();
if (equals(asCanonical)) {
//noinspection unchecked
return (T) this;
}
return asCanonical;
}
protected abstract T asCanonical();
}
As you can see the toCanonical()
method compare itself with asCanonical
build calling the abstract method asCanonical()
.
To test this I need to create an empty implementation of my abstract class and make mockito calls real methods for the two implemented method and return my own classes for the asCanonical().
BaseClass aCanonicalClass;
BaseClass aNonCanonicalClass;
@Mock
BaseClass sut;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
// these are the methods under test, use the real ones
Mockito.when(sut.isCanonical())
.thenCallRealMethod();
Mockito.when(sut.toCanonical())
.thenCallRealMethod();
}
Normally if this wasn't the equals()
method I would just do this:
@Test
public void test_isCanonical_bad() {
// test true
Mockito.when(sut.equals(aCanonicalClass))
.thenReturn(true);
Mockito.when(sut.equals(aNonCanonicalClass))
.thenReturn(false);
Mockito.when(sut.asCanonical())
.thenReturn(aCanonicalClass);
Assert.assertTrue(sut.isCanonical());
// test false
Mockito.when(sut.equals(aNonCanonicalClass))
.thenReturn(true);
Mockito.when(sut.equals(aCanonicalClass))
.thenReturn(false);
Mockito.when(sut.asCanonical())
.thenReturn(aCanonicalClass);
Assert.assertFalse(sut.isCanonical());
}
Which give this error:
org.mockito.exceptions.misusing.MissingMethodInvocationException: when() requires an argument which has to be 'a method call on a mock'. For example: when(mock.getArticles()).thenReturn(articles); Also, this error might show up because: 1. you stub either of: final/private/equals()/hashCode() methods. Those methods *cannot* be stubbed/verified. Mocking methods declared on non-public parent classes is not supported. 2. inside when() you don't call method on mock but on some other object.
One of Mockito limitations is that it doesn't allow to mock the equals()
and hashcode()
methods.
As a workaround I just pass itself when I want to test the condition equals() = true
and another random object when I want to test the condition equals() = false
.
@Test
public void test_isCanonical() {
// test true
Mockito.when(sut.asCanonical())
.thenReturn(sut);
Assert.assertTrue(sut.isCanonical());
// test false
Mockito.when(sut.asCanonical())
.thenReturn(aCanonicalClass);
Assert.assertFalse(sut.isCanonical());
}
But this test pass even if I change the implementation to this:
public T toCanonical() {
T asCanonical = asCanonical();
if (this == asCanonical) {
//noinspection unchecked
return (T) this;
}
return asCanonical;
}
or even this:
public T toCanonical() {
return asCanonical();
}
which is wrong! Comparison should be done with equals. Doing so by reference is not the same thing.
Things gets impossible to test when I get to the toCanonical()
method:
@Test
public void test_toCanonical_bad() {
// test canonical
Mockito.when(sut.equals(aCanonicalClass))
.thenReturn(true);
Mockito.when(sut.equals(aNonCanonicalClass))
.thenReturn(false);
Mockito.when(sut.asCanonical())
.thenReturn(aCanonicalClass);
Assert.assertSame(sut, sut.toCanonical());
// test non-canonical
Mockito.when(sut.equals(aNonCanonicalClass))
.thenReturn(true);
Mockito.when(sut.equals(aCanonicalClass))
.thenReturn(false);
Mockito.when(sut.asCanonical())
.thenReturn(aCanonicalClass);
Assert.assertNotEquals(sut, sut.toCanonical());
Assert.assertSame(aCanonicalClass, sut.toCanonical());
}
This of course doesn't work, and doesn't really makes sense to apply the same workaround because I would just test that the return value of the function is the one of asCanonical()
:
@Test
public void test_toCanonical() {
// test canonical
Mockito.when(sut.asCanonical())
.thenReturn(sut);
Assert.assertSame(sut, sut.toCanonical());
// test non-canonical
Mockito.when(sut.asCanonical())
.thenReturn(aCanonicalClass);
Assert.assertNotEquals(sut, sut.toCanonical());
Assert.assertSame(aCanonicalClass, sut.toCanonical());
}
Both test are pointless because of the fact I can't mock the result of equals()
.
Is there a way to test at least equals()
is being called with the object I gave it? (spy it)
Is there any other way to test this?
EDIT: this is a semplification of the actual code I'm working with.
I modified the example code to at least show the
toCanonical()
andasCanonical()
methods are supposed to return instances of the same class (and not just any class)I want to test the base class here. Only the concrete implementation know how to build an actual canonical class (
asCanonical()
) or check if this class is equal to a canonical class (equals()
). And please note thattoCanonical()
return ITSELF if it is equals to the canonical version of itself. Againequals != same
.The actual implementations are immutable classes. And since they are many and this code is the same for everyone i put it in a base class.
Upvotes: 3
Views: 1917
Reputation: 4791
Why would you test an abstract class? If you really want to test the toCanonical
method (which is not final
, so you can override it later) you can instantiate a temp concrete class and test it. That is way easier then using Mockito and other stuff.
Ie:
@Test
public void myAwesomeTest() {
// Arrange
BaseClass test = new BaseClass<String>() {
// provide concrete implementation
protected String asCanonical() {
return "Foo";
}
};
// Act
String result = test.toCanonical("Bar");
// Assert
}
Once the simple case works, you can extract the creation of baseclass
into a factory method ie MakeBaseClass(String valueToReturnForAsCanonical
and write more tests.
Using Spy's are handy if you are in some regions of untested code and you have no way to stub out parts or create some seams in the code. In this case I think you can suffice with writing the boilerplate yourself instead of relying on some magic by Mockito.
Good luck!
Upvotes: 1