如何使用Mockito对依赖equals()方法的抽象类方法进行单元测试?

3

我需要在Java中测试一个抽象类,我有一个方法依赖于equals()方法来提供结果。

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();
}

正如您所看到的,toCanonical() 方法会将自身与调用抽象方法 asCanonical() 所构建的 asCanonical 进行比较。

为了测试这个方法,我需要创建一个空实现的抽象类,并且使用 mockito 调用两个已实现方法的真实方法,并且为 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();
}

通常如果这不是 equals() 方法,我只需要这样做:
@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());
}

这会产生以下错误:

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.

Mockito的一个限制是它不允许模拟equals()hashcode()方法。

作为一种解决方法,当我想测试条件equals() = true时,我只需传递本身,而当我想测试条件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());
}

但是即使我将实现更改为以下内容,此测试也会通过:
    public T toCanonical() {
        T asCanonical = asCanonical();
        if (this == asCanonical) {
            //noinspection unchecked
            return (T) this;
        }
        return asCanonical;
    }

甚至可以这样做:
    public T toCanonical() {
        return asCanonical();
    }

这是错误的!应该使用equals进行比较。通过引用进行比较并不是同一件事。

当我到达toCanonical()方法时,测试变得不可能:

@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());
}

当然,这样做是行不通的,而且应用同样的解决方法也没有意义,因为我只需测试函数的返回值是否为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());
}

由于我无法模拟equals()的结果,因此这两个测试都是无意义的。是否有一种方法可以测试至少使用我提供的对象调用了equals()?(进行间谍操作)还有其他测试方法吗?

编辑:这只是我正在处理的实际代码的简化版本。

我修改了示例代码,以至少显示toCanonical()asCanonical()方法应返回相同类的实例(而不仅仅是任何类)。

我想在这里测试基类。只有具体实现知道如何构建实际的规范类(asCanonical()),或者检查该类是否等于规范类(equals())。请注意,如果toCanonical()等于其规范版本,则返回ITSELF。再次强调equals != same

实际实现是不可变类。由于它们很多,而且这段代码对每个人都是相同的,所以我将它放在了基类中。


我真的不明白为什么你不能创建BaseClass的非空实现并在那里放置所有必要的测试逻辑?目前看起来,你似乎因某种原因禁止自己在没有Mockito的情况下设置测试。 - user3707125
这是我从我的实际代码中缩小范围以提出问题的简化代码。我没有一个BaseClass的单一实现,为了测试我的抽象类是否按照预期执行,我必须测试一个空实现。如果子类想要覆盖toCanonical(),那么它可以自由地这样做,并且将有自己的测试。 - Daniele Segato
1个回答

1
为什么要测试抽象类?如果您真的想测试toCanonical方法(它不是final,所以您可以稍后覆盖它),您可以实例化一个临时具体类并对其进行测试。这比使用Mockito和其他工具要容易得多。
例如:
@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

}

当简单情况可行时,您可以将baseclass的创建提取到一个工厂方法中,即MakeBaseClass(String valueToReturnForAsCanonical),并编写更多的测试。

如果您处于一些未经测试的代码区域,并且没有办法存根部分或在代码中创建一些接缝,则使用Spy非常方便。在这种情况下,我认为您可以自己编写样板代码,而不是依赖Mockito的某些魔法。

祝你好运!


我不明白这个测试如何检验 toCanonical() 函数。该函数的作用是调用 asCanonical(),如果返回值与自身相等,则返回自身,否则返回生成的值。只有具体类知道如何生成规范版本或者知道两个类何时相同。我已经编辑了我的问题,使其更接近我实际拥有的代码(当然这只是一个简化版)。 - Daniele Segato
请检查我的编辑,我添加了另一部分,toCanonical() 函数测试并修复了简化示例。 - Daniele Segato
测试调用toCanonical(),该方法又调用asCanonical。无论如何,您仍应该能够实例化一个具体类并对其进行测试。或者,使用匿名类。 - Stefan Hendriks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接