简体   繁体   中英

How to unit test an abstract class method that depends on equals() with Mockito?

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() and asCanonical() 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 that toCanonical() return ITSELF if it is equals to the canonical version of itself. Again equals != 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.

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!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM