継承されたメソッドをテストする必要がありますか?


30

基本クラスEmployeeから派生したクラスManagerがあり、そのEmployeeにはManagerによって継承されるメソッドgetEmail()があるとします。マネージャーのgetEmail()メソッドの動作が実際に従業員のものと同じであることをテストする必要がありますか?

これらのテストが作成された時点で動作は同じになりますが、もちろん将来のある時点で誰かがこのメソッドをオーバーライドし、その動作を変更して、アプリケーションを中断するかもしれません。しかし、本質的に干渉コードがないことをテストするのは少し奇妙に思えます。

Manager :: getEmail()メソッドをテストしても、Manager :: getEmail()が作成/オーバーライドされるまで、コードカバレッジ(または他のコード品質メトリック(?))は改善されません。)

(答えが「はい」の場合、基本クラスと派生クラス間で共有されるテストの管理方法に関する情報が役立ちます。)

質問の同等の定式化:

派生クラスが基本クラスからメソッドを継承する場合、継承されたメソッドが次のことを期待しているかどうかをどのように表現(テスト)しますか?

  1. 現在のベースとまったく同じように動作します(ベースの動作が変更されても、派生メソッドの動作は変更されません)。
  2. 常にベースとまったく同じように動作します(ベースクラスの動作が変わると、派生クラスの動作も変わります)。または
  3. ただし、必要に応じて動作します(呼び出さないため、このメソッドの動作は気にしません)。

1
IMOからManagerクラスを派生させることEmployeeは、最初の大きな間違いでした。
CodesInChaos

4
@CodesInChaosはい、それは悪い例かもしれません。ただし、継承がある場合は常に同じ問題が適用されます。
mjs

1
メソッドをオーバーライドする必要さえありません。基本クラスのメソッドは、オーバーライドされている他のインスタンスメソッドを呼び出すことができますが、メソッドに対して同じソースコードを実行しても、異なる動作が作成されます。
gnasher729

回答:


23

ここで実際的なアプローチを取ります。将来、誰かがManager :: getMailをオーバーライドする場合、新しいメソッドのテストコードを提供する開発者の責任です。

もちろん、Manager::getEmail実際に同じコードパスがある場合にのみ有効ですEmployee::getEmail!メソッドがオーバーライドされない場合でも、動作が異なる場合があります。

  • Employee::getEmailオーバーライドされる保護された仮想getInternalEmailを呼び出すことができます。Manager
  • Employee::getEmail_email2つの実装で異なる可能性のあるいくつかの内部状態(たとえば、いくつかのフィールド)にアクセスできます。たとえば、デフォルトの実装では、常にであるEmployeeことが保証され_emailますがfirstname.lastname@example.comManagerメールアドレスの割り当てはより柔軟です。

そのような場合、メソッド自体の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

1
よく置きます。Managerが継承されたメソッドを使用しているという理由だけで、テストされるべきではないという意味ではありません。私の偉大なリーダーは、かつて「テストされていなければ壊れている」と言っていました。そのため、コードチェックインで必要なEmployeeとManagerのテストを行う場合、継承されたメソッドの動作を変更する可能性があるManagerの新しいコードをチェックインする開発者は、新しい動作を反映するようにテストを修正します。 。または、ドアをノックしてください。
ハリケーン

2
あなたは本当にManagerクラスをテストしたいのです。Employeeのサブクラスであり、getMail()が継承に依存して実装されているという事実は、単体テストを作成するときに無視すべき実装の詳細にすぎません。来月、従業員からManagerを継承するのは悪い考えであり、継承構造全体を置き換えて、すべてのメソッドを再実装することがわかりました。単体テストでは、問題なくコードのテストを続行する必要があります。
gnasher729

5

単体テストをしないでください。機能/受け入れテストを行います。

ユニットテストでは、すべての実装をテストする必要があります。新しい実装を提供しない場合は、DRY原則に従ってください。ここで何らかの努力をしたい場合は、元の単体テストを強化できます。メソッドをオーバーライドする場合にのみ、単体テストを作成してください。

同時に、機能/受け入れテストでは、1日の終わりにすべてのコードが想定どおりに動作することを確認する必要があります。


4

TDDに関するRobert Martinのルールは次のとおりです。

  1. 失敗した単体テストに合格しない限り、実稼働コードを作成することはできません。
  2. 失敗するのに十分な数以上の単体テストを作成することはできません。コンパイルの失敗は失敗です。
  3. 1つの失敗した単体テストに合格するのに十分な量を超える生産コードを記述することはできません。

これらの規則に従った場合、この質問をする立場に立つことはできません。getEmailメソッドをテストする必要がある場合、テストされているはずです。


3

はい、継承されたメソッドは将来オーバーライドされる可能性があるため、テストする必要があります。また、継承されたメソッドは、オーバーライドされた仮想メソッドを呼び出すことができます。これにより、オーバーライドされていない継承されたメソッドの動作が変更されます。

私がこれをテストする方法は、抽象クラスを作成して(おそらく抽象)基本クラスまたはインターフェイスをテストすることです(NUnitを使用したC#で)。

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

私がそうすることができる理由は、リスコフの代用原理です:オブジェクトをEmployee渡すときのテストはManager、から派生してEmployeeいるため、パスする必要があります。そのため、テストを1回だけ記述する必要があり、インターフェイスまたは基本クラスのすべての可能な実装に対してテストが機能することを検証できます。


良い点は、リスコフの置換原理についてです。これが成り立つ場合、派生クラスはすべての基本クラスのテストに合格することは正しいです。ただし、xUnitのsetUp()メソッド自体を含め、LSPは頻繁に違反されます!また、「インデックス」メソッドのオーバーライドを伴うほとんどすべてのWeb MVCフレームワークも、基本的にはすべてであるLSPを破壊します。
mjs

これは絶対に正しい答えです-「抽象テストパターン」と呼ばれると聞きました。好奇心から、なぜテストを単に混合するのではなく、ネストされたクラスを使用するのclass ManagerTests : ExployeeTestsですか?(すなわち、テスト中のクラスの継承をミラーリングします。)結果を見るのを助けるのは、主に美学ですか?
ルークアッシャーウッド

2

マネージャーのgetEmail()メソッドの動作が実際に従業員の動作と同じであることをテストする必要がありますか?

私は、従業員テストで一度テストするという私の意見では繰り返しテストになるので、ノーと言うでしょう。

これらのテストが作成された時点では動作は同じですが、もちろん将来のある時点で誰かがこのメソッドをオーバーライドして動作を変更する可能性があります

メソッドがオーバーライドされた場合、オーバーライドされた動作をチェックする新しいテストが必要になります。それは、オーバーライド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が失敗するとYは失敗するので、Xをテストする必要はありません」と言います。ただし、単体テストは、コード内のバグの仮定に基づいています。コードにバグがある場合、テスト間の複雑な関係が期待どおりに機能すると思うのはなぜですか?
gnasher729

@ gnasher729「スーパークラスの単体テストですでにテストされているため、サブクラスでXをテストする必要はありません」と言います。もちろん、Xの実装を変更する場合、適切なテストを作成する必要があります。
Uooo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.