Tôi có nên thử nghiệm các phương pháp kế thừa?


30

Giả sử tôi có Trình quản lý lớp xuất phát từ Nhân viên lớp cơ sở và Nhân viên đó có phương thức getEmail () được thừa kế bởi Người quản lý . Tôi có nên kiểm tra xem hành vi của phương thức getEmail () của người quản lý trên thực tế có giống với nhân viên không?

Tại thời điểm các bài kiểm tra này được viết, hành vi sẽ giống nhau, nhưng tất nhiên tại một thời điểm nào đó trong tương lai ai đó có thể ghi đè phương thức này, thay đổi hành vi của nó và do đó phá vỡ ứng dụng của tôi. Tuy nhiên, có vẻ hơi lạ khi về cơ bản kiểm tra sự vắng mặt của mã trung gian.

(Lưu ý rằng phương thức tests Manager :: getEmail () không cải thiện phạm vi bảo hiểm mã (hoặc thực tế là bất kỳ số liệu chất lượng mã nào khác (?)) Cho đến khi Manager :: getEmail () được tạo / ghi đè.)

(Nếu câu trả lời là "Có", một số thông tin về cách quản lý các bài kiểm tra được chia sẻ giữa các lớp cơ sở và các lớp dẫn xuất sẽ hữu ích.)

Một công thức tương đương của câu hỏi:

Nếu một lớp dẫn xuất kế thừa một phương thức từ một lớp cơ sở, làm thế nào để bạn thể hiện (kiểm tra) xem bạn có mong đợi phương thức được kế thừa để:

  1. Hành xử theo cách chính xác giống như cơ sở hiện tại (nếu hành vi của cơ sở thay đổi, hành vi của phương thức dẫn xuất không);
  2. Hành xử chính xác giống như cơ sở cho mọi thời đại (nếu hành vi của lớp cơ sở thay đổi, hành vi của lớp dẫn xuất cũng thay đổi); hoặc là
  3. Hành vi tuy nhiên nó muốn (bạn không quan tâm đến hành vi của phương pháp này vì bạn không bao giờ gọi nó).

1
IMO bắt nguồn từ một Managerlớp học Employeelà sai lầm lớn đầu tiên.
CodeInChaos

4
@CodesInChaos Vâng, đó có thể là một ví dụ tồi. Nhưng vấn đề tương tự áp dụng bất cứ khi nào bạn có quyền thừa kế.
mjs

1
Bạn thậm chí không cần ghi đè phương thức. Phương thức lớp cơ sở có thể gọi các phương thức cá thể khác bị ghi đè và thực thi cùng một mã nguồn cho phương thức vẫn tạo ra hành vi khác nhau.
gnasher729

Câu trả lời:


23

Tôi sẽ áp dụng cách tiếp cận thực tế ở đây: Nếu ai đó, trong tương lai, ghi đè Trình quản lý :: getMail, thì trách nhiệm của nhà phát triển là cung cấp mã kiểm tra cho phương thức mới.

Tất nhiên điều đó chỉ hợp lệ nếu Manager::getEmailthực sự có cùng đường dẫn mã như Employee::getEmail! Ngay cả khi phương thức không bị ghi đè, nó có thể hoạt động khác đi:

  • Employee::getEmailcó thể gọi một số ảo được bảo vệ getInternalEmailđược ghi đè trong Manager.
  • Employee::getEmailcó thể truy cập một số trạng thái nội bộ (ví dụ: một số trường _email), có thể khác nhau trong hai triển khai: Ví dụ: việc triển khai mặc định Employeecó thể đảm bảo _emailluôn luôn firstname.lastname@example.com, nhưng Managerlinh hoạt hơn trong việc gán địa chỉ thư.

Trong các trường hợp như vậy, có thể một lỗi chỉ xuất hiện trong đó Manager::getEmail, mặc dù việc triển khai phương thức là như nhau. Trong trường hợp đó thử nghiệm Manager::getEmailriêng biệt có thể có ý nghĩa.


14

Tôi sẽ.

Nếu bạn đang nghĩ "tốt, thực sự chỉ gọi Employee :: getEmail () vì tôi không ghi đè lên nó, vì vậy tôi không cần kiểm tra Manager :: getEmail ()" thì bạn không thực sự kiểm tra hành vi của người quản lý :: getEmail ().

Tôi sẽ nghĩ về những gì chỉ Người quản lý :: getEmail () nên làm, không biết nó có được thừa kế hay ghi đè hay không. Nếu hành vi của Manager :: getEmail () phải trả về bất cứ điều gì Employee :: getMail () trả về, thì đó là thử nghiệm. Nếu hành vi là trả về "Pink@unicorns.com", thì đó là thử nghiệm. Nó không quan trọng cho dù nó được thực hiện bởi thừa kế hoặc ghi đè.

Vấn đề là ở chỗ, nếu nó thay đổi trong tương lai, bài kiểm tra của bạn sẽ nắm bắt được và bạn biết điều gì đó đã bị hỏng hoặc cần phải xem xét lại.

Một số người có thể không đồng ý với sự dư thừa dường như trong séc, nhưng đối tác của tôi với điều đó là bạn đang kiểm tra hành vi của Employee :: getMail () và Manager :: getMail () như các phương thức riêng biệt, cho dù họ có được thừa kế hay không ghi đè. Nếu một nhà phát triển trong tương lai cần thay đổi hành vi của Manager :: getMail () thì họ cũng cần cập nhật các thử nghiệm.

Ý kiến ​​có thể khác nhau mặc dù, tôi nghĩ rằng Digger và Heinzi đã đưa ra những lời biện minh hợp lý cho điều ngược lại.


1
Tôi thích lập luận rằng kiểm tra hành vi là trực giao với cách thức lớp được xây dựng. Mặc dù có vẻ hơi buồn cười khi hoàn toàn bỏ qua thừa kế. (Và làm thế nào để bạn quản lý các bài kiểm tra được chia sẻ, nếu bạn có nhiều quyền thừa kế?)
mjs

1
Vâng đặt. Chỉ vì Trình quản lý đang sử dụng một phương thức được kế thừa, không có nghĩa là nó không nên được kiểm tra. Một người lãnh đạo tuyệt vời của tôi từng nói: "Nếu nó không được thử nghiệm, nó đã bị hỏng". Cuối cùng, nếu bạn thực hiện các bài kiểm tra cho Nhân viên và Người quản lý cần thiết khi đăng ký mã, bạn sẽ chắc chắn rằng nhà phát triển kiểm tra mã mới cho Trình quản lý có thể thay đổi hành vi của phương thức được kế thừa sẽ sửa lỗi kiểm tra để phản ánh hành vi mới . Hoặc đến gõ cửa nhà bạn.
cơn

2
Bạn thực sự muốn kiểm tra lớp Manager. Thực tế đó là một lớp con của Nhân viên và getMail () được triển khai bằng cách dựa vào tính kế thừa, chỉ là một chi tiết triển khai mà bạn nên bỏ qua khi tạo các bài kiểm tra đơn vị. Tháng tới bạn phát hiện ra rằng Người quản lý kế thừa từ Nhân viên là một ý tưởng tồi và bạn thay thế toàn bộ cấu trúc thừa kế và thực hiện lại tất cả các phương thức. Kiểm tra đơn vị của bạn nên tiếp tục kiểm tra mã của bạn mà không có vấn đề.
gnasher729

5

Đừng đơn vị kiểm tra nó. Làm chức năng / chấp nhận kiểm tra nó.

Các bài kiểm tra đơn vị nên kiểm tra mọi triển khai, nếu bạn không cung cấp một triển khai mới thì hãy tuân thủ nguyên tắc DRY. Nếu bạn muốn dành một số nỗ lực ở đây thì bạn có thể tăng cường kiểm tra đơn vị ban đầu. Chỉ khi bạn ghi đè phương thức, bạn mới nên viết một bài kiểm tra đơn vị.

Đồng thời, kiểm tra chức năng / chấp nhận phải đảm bảo rằng vào cuối ngày, tất cả các mã của bạn sẽ thực hiện đúng như mong muốn và hy vọng sẽ bắt được bất kỳ sự kỳ lạ nào từ kế thừa.


4

Các quy tắc của Robert Martin cho TDD là:

  1. Bạn không được phép viết bất kỳ mã sản xuất nào trừ khi nó không vượt qua bài kiểm tra đơn vị.
  2. Bạn không được phép viết thêm bất kỳ bài kiểm tra đơn vị nào là đủ để thất bại; và thất bại biên dịch là thất bại.
  3. Bạn không được phép viết thêm bất kỳ mã sản xuất nào đủ để vượt qua bài kiểm tra đơn vị thất bại.

Nếu bạn tuân theo các quy tắc này, bạn sẽ không thể vào vị trí để đặt câu hỏi này. Nếu getEmailphương pháp cần được kiểm tra, nó sẽ được kiểm tra.


3

Có, bạn nên kiểm tra các phương thức được kế thừa vì trong tương lai chúng có thể bị ghi đè. Ngoài ra, các phương thức được kế thừa có thể gọi các phương thức ảo bị ghi đè , điều này sẽ thay đổi hành vi của phương thức kế thừa không bị ghi đè.

Cách tôi kiểm tra điều này là bằng cách tạo một lớp trừu tượng để kiểm tra lớp cơ sở (có thể là trừu tượng), như thế này (trong C # bằng 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));
    }
}

Sau đó, tôi có một lớp với các bài kiểm tra dành riêng cho Managervà tích hợp các bài kiểm tra của nhân viên như thế này:

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

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

Lý do tôi có thể vì vậy đây là nguyên tắc thay thế của Liskov: các bài kiểm tra Employeevẫn nên vượt qua khi vượt qua một Managerđối tượng, vì nó xuất phát từ Employee. Vì vậy, tôi phải viết các bài kiểm tra của mình chỉ một lần và có thể xác minh chúng hoạt động cho tất cả các triển khai có thể có của giao diện hoặc lớp cơ sở.


Điểm hay của nguyên tắc thay thế của Liskov, bạn đúng rằng nếu điều này được giữ vững, lớp dẫn xuất sẽ vượt qua tất cả các bài kiểm tra của lớp cơ sở. Tuy nhiên, LSP thường xuyên bị vi phạm, kể cả bởi setUp()chính phương thức của xUnit ! Và hầu như mọi khung công tác web MVC liên quan đến việc ghi đè phương thức "chỉ mục" cũng phá vỡ LSP, về cơ bản là tất cả chúng.
mjs

Đây hoàn toàn là câu trả lời đúng - tôi đã nghe nó được gọi là "Mẫu thử nghiệm trừu tượng". Vì tò mò, tại sao lại sử dụng một lớp lồng nhau thay vì chỉ trộn lẫn các bài kiểm tra thông qua class ManagerTests : ExployeeTests? (tức là Phản ánh sự kế thừa của các lớp đang thử nghiệm.) Có phải chủ yếu là thẩm mỹ, để hỗ trợ cho việc xem kết quả?
Luke Usherwood

2

Tôi có nên kiểm tra xem hành vi của phương thức getEmail () của người quản lý trên thực tế có giống với nhân viên không?

Tôi sẽ nói không vì đó sẽ là một bài kiểm tra lặp đi lặp lại theo ý kiến ​​của tôi, tôi sẽ kiểm tra một lần trong các bài kiểm tra của Nhân viên và đó sẽ là bài kiểm tra đó.

Tại thời điểm các bài kiểm tra này được viết, hành vi sẽ giống nhau, nhưng tất nhiên tại một thời điểm nào đó trong tương lai ai đó có thể ghi đè phương thức này, thay đổi hành vi của nó

Nếu phương thức bị ghi đè thì bạn sẽ cần các thử nghiệm mới để kiểm tra hành vi bị ghi đè. Đó là công việc của người thực hiện getEmail()phương pháp ghi đè .


1

Không, bạn không cần phải kiểm tra các phương thức kế thừa. Các lớp và các trường hợp thử nghiệm của chúng dựa trên phương thức này sẽ bị hỏng nếu hành vi thay đổi Manager.

Hãy nghĩ về kịch bản sau đây: Địa chỉ email được tập hợp thành Firstname.Lastname@example.com:

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

Bạn đã kiểm tra đơn vị này và nó hoạt động tốt cho bạn Employee. Bạn cũng đã tạo một lớp Manager:

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

Bây giờ bạn có một lớp ManagerSortsắp xếp các nhà quản lý trong danh sách dựa trên địa chỉ email của họ. Trong số các bạn, bạn cho rằng việc tạo email giống như trong Employee:

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

Bạn viết một bài kiểm tra cho ManagerSort:

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

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

Mọi thứ đều hoạt động tốt. Bây giờ ai đó đến và ghi đè getEmail()phương thức:

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

Bây giờ, chuyện gì xảy ra? Bạn testManagerSort()sẽ thất bại vì getEmail()các Managerbị ghi đè. Bạn sẽ điều tra trong vấn đề này và sẽ tìm ra nguyên nhân. Và tất cả mà không cần viết một testcase riêng cho phương thức kế thừa.

Do đó, bạn không cần phải thử nghiệm các phương thức kế thừa.

Mặt khác, ví dụ trong Java, bạn sẽ phải kiểm tra tất cả các phương thức được kế thừa từ Objectlike toString(), equals()v.v. trong mỗi lớp.


Bạn không cần phải thông minh với các bài kiểm tra đơn vị. Bạn nói "Tôi không cần kiểm tra X vì khi X thất bại, Y thất bại". Nhưng các bài kiểm tra đơn vị dựa trên các giả định về lỗi trong mã của bạn. Với các lỗi trong mã của bạn, tại sao bạn lại nghĩ rằng mối quan hệ phức tạp giữa các bài kiểm tra hoạt động theo cách bạn mong đợi chúng hoạt động?
gnasher729

@ gnasher729 Tôi nói "Tôi không cần kiểm tra X trong lớp con vì nó đã được thử nghiệm trong các bài kiểm tra đơn vị cho siêu lớp ". Tất nhiên, nếu bạn thay đổi việc thực hiện X, bạn phải viết các bài kiểm tra phù hợp cho nó.
Uooo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.