我应该测试继承的方法吗?


30

假设我有一个从基类Employee派生的类Manager,并且该Employee有一个由Manager继承的方法getEmail()。我是否应该测试经理的getEmail()方法的行为实际上与员工的行为相同?

在编写这些测试时,其行为将是相同的,但是当然在将来的某个时候,有人可能会重写此方法,更改其行为,从而破坏我的应用程序。但是,从根本上测试是否存在干预代码似乎有些奇怪。

(请注意,在创建/覆盖Manager :: getEmail()之前,测试Manager :: getEmail()方法不会提高代码覆盖率(或其他任何代码质量指标(?))。

(如果答案是“是”,则有关如何管理基类和派生类之间共享的测试的一些信息将很有用。)

问题的等价表述:

如果派生类从基类继承方法,则如何表达(测试)是否期望继承的方法:

  1. 行为与现在的基础完全相同(如果基础的行为发生了变化,则派生方法的行为不会发生变化);
  2. 始终具有与基类完全相同的方式(如果基类的行为发生变化,派生类的行为也发生变化);要么
  3. 但是,它要表现出来(您不必关心此方法的行为,因为您从不调用它)。

1
IMO Manager从中得出一个类Employee是第一个主要错误。
CodesInChaos

4
@CodesInChaos是的,那可能是一个不好的例子。但是,只要有继承,就会出现相同的问题。
mjs

1
您甚至不需要重写该方法。基类方法可以调用其他实例方法,这些方法将被重写,并且为该方法执行相同的源代码仍然会产生不同的行为。
gnasher729 2014年

Answers:


23

我在这里采用务实的方法:如果将来有人重写Manager :: getMail,那么开发人员有责任为新方法提供测试代码。

当然,这仅在Manager::getEmail确实具有与Employee::getEmail!相同的代码路径时才有效。即使未重写该方法,其行为也可能有所不同:

  • Employee::getEmail可以打电话给一些受保护的虚拟getInternalEmail在覆盖Manager
  • Employee::getEmail可以访问某些内部状态(例如某些字段_email),这在两种实现中可能有所不同:例如,的默认实现Employee可以确保_email始终为firstname.lastname@example.com,但是Manager在分配邮件地址时更加灵活。

在这种情况下,Manager::getEmail即使方法本身的实现是相同的,错误也可能仅在中出现。在这种情况下,Manager::getEmail单独进行测试可能很有意义。


14

我会。

如果您在考虑“好吧,因为我没有覆盖它,所以它实际上只是在调用Employee :: getEmail(),所以我不需要测试Manager :: getEmail()”,那么您就不是在真正测试行为Manager :: getEmail()。

我会想到只有Manager :: getEmail()才应该做的事情,而不管它是否被继承或重写。如果Manager :: getEmail()的行为应返回Employee :: getMail()返回的值,那么就是测试。如果行为是返回“ pink@unicorns.com”,那就是测试。它是通过继承实现还是重写实现都没有关系。

重要的是,如果将来发生更改,您的测试会抓住它,并且您知道某些内容已损坏或需要重新考虑。

某些人可能不同意支票中似乎存在的冗余,但我的反对意见是,您正在将行为Employee :: getMail()和Manager :: getMail()作为不同的方法进行测试,无论它们是否被继承或覆盖。如果将来的开发人员需要更改Manager :: getMail()的行为,那么他们还需要更新测试。

虽然意见可能会有所不同,但我认为Digger和Heinzi给出了相反的合理理由。


1
我喜欢行为测试与类的构造方式正交的说法。但是,完全忽略继承似乎有些可笑。(如果您有很多继承关系,又该如何管理共享测试?)
mjs 2013年

1
说得好。仅仅因为Manager正在使用继承的方法,决不意味着不应对其进行测试。我的一位伟大的负责人曾经说过:“如果不测试,那就坏了。” 为此,如果您对代码签入进行了针对Employee和Manager的测试,那么您将确保开发人员签入可能更改继承方法行为的Manager的新代码将修复该测试以反映新行为。或敲门。
飓风

2
您真的想测试Manager类。它是Employee的子类,并且getMail()是通过继承实现的,这只是在创建单元测试时应忽略的实现细节。下个月,您发现从Employee继承Manager是一个坏主意,您将替换整个继承结构并重新实现所有方法。您的单元测试应该继续测试您的代码而不会出现问题。
gnasher729 2014年

5

不要对它进行单元测试。做功能/验收测试。

单元测试应该测试每个实现,如果您不提供新的实现,则坚持DRY原则。如果您想在这里花费一些精力,则可以增强原始的单元测试。只有重写该方法,才可以编写单元测试。

同时,功能/验收测试应确保在一天结束时所有代码都按预期进行,并希望从继承中捕获任何怪异之处。


4

罗伯特·马丁关于TDD的规则是:

  1. 除非要通过失败的单元测试,否则不允许编写任何生产代码。
  2. 您不能编写任何足以使单元测试失败的单元测试。而编译失败就是失败。
  3. 您不能编写任何足以通过一项失败的单元测试的生产代码。

如果遵守这些规则,您将无法提出这个问题。如果getEmail需要测试该方法,则该方法已经过测试。


3

是的,您应该测试继承的方法,因为将来它们可能会被覆盖。同样,继承的方法可能会调用被覆盖的虚拟方法,这会改变非覆盖的继承方法的行为。

我测试此方法的方式是,通过创建一个抽象类来测试(可能是抽象的)基类或接口,如下所示(在C#中使用NUnit):

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

然后,我有一个包含特定于的测试的类Manager,并集成了员工的测试,如下所示:

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

我之所以这样,是因为Liskov的替换原则:Employee传递Manager对象时的测试仍应通过,因为它源自Employee。因此,我只需要编写一次测试,就可以验证它们对接口或基类的所有可能实现均有效。


关于Liskov的替换原理,您说得对,如果正确,派生类将通过所有基类的测试,这是正确的。但是,经常违反LSP,包括xUnit setUp()方法本身!几乎所有涉及重写“索引”方法的Web MVC框架也都会破坏LSP,而LSP基本上就是它们。
mjs 2013年

这绝对是正确的答案-我听说过它叫做“抽象测试模式”。出于好奇,为什么要使用嵌套类而不是仅仅通过来混合测试class ManagerTests : ExployeeTests?(即,反映被测类的继承。)主要是美观,以帮助查看结果吗?
Luke Usherwood

2

我是否应该测试经理的getEmail()方法的行为实际上与员工的行为相同?

我会说不,因为我认为这将是重复测试,因此我将在Employee测试中测试一次,仅此而已。

在编写这些测试时,其行为将是相同的,但是当然在将来的某个时候,有人可能会覆盖此方法,更改其行为

如果该方法被覆盖,那么您将需要新的测试来检查被覆盖的行为。那是实现覆盖getEmail()方法的人的工作。


1

不,您不需要测试继承的方法。如果行为改变了,依赖这种方法的类及其测试用例将会中断Manager

考虑以下情形:电子邮件地址被汇编为Firstname.Lastname@example.com:

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

您已经对此进行了单元测试,它对您的工作正常Employee。您还创建了一个类Manager

class Manager extends Employee { /* nothing different in email generation */ }

现在,您有了一个ManagerSort可根据经理的电子邮件地址对经理进行排序的类。您认为您的电子邮件生成与中的相同Employee

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

您为您编写了一个测试ManagerSort

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

一切正常。现在有人来覆盖该getEmail()方法:

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

现在,会发生什么?testManagerSort()由于的getEmail()Manager被覆盖,您将失败。您将对此问题进行调查并找到原因。所有这些都无需为继承的方法编写单独的测试用例。

因此,您不需要测试继承的方法。

否则,如在Java中,你必须测试来自继承的方法Object一样toString()equals()等在每一个类。


您不应该对单元测试很聪明。您说“我不需要测试X,因为当X失败时,Y就会失败”。但是单元测试是基于代码中错误的假设。有了代码中的错误,您为什么会认为测试之间的复杂关系以您期望的方式工作?
gnasher729 2014年

@ gnasher729我说:“我不需要在子类中测试X,因为它已经在超类的单元测试中进行了测试 ”。当然,如果更改X的实现,则必须为其编写适当的测试。
2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.